Skip to content

Commit faf063e

Browse files
committed
Implement support for pasting images in the chat panel, rendering them as interactive symbols, and processing them for multimodal model requests
1 parent a69a99e commit faf063e

25 files changed

Lines changed: 698 additions & 228 deletions

‎apps/editor/src/commands/apply-context-command/sources/check-all-with-keywords-source.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const handle_check_all_with_keywords_source = async (
1717
input_box.title = 'Keyword Search'
1818
input_box.prompt =
1919
'Enter keywords separated by comma (use & for AND, quotes for whole words)'
20-
input_box.placeholder = 'e.g. user, login&auth, "ignoreFocus"'
20+
input_box.placeholder = 'e.g. user, login&auth, "changePassword"'
2121
input_box.ignoreFocusOut = true
2222
input_box.buttons = [vscode.QuickInputButtons.Back]
2323

@@ -221,6 +221,7 @@ export const handle_check_all_with_keywords_source = async (
221221
quick_pick_merge.items = quick_pick_options
222222
quick_pick_merge.placeholder = `How would you like to apply the ${selected_paths.length} selected files?`
223223
quick_pick_merge.buttons = [vscode.QuickInputButtons.Back]
224+
quick_pick_merge.ignoreFocusOut = true
224225

225226
if (last_choice_label) {
226227
const active_item = quick_pick_options.find(

‎apps/editor/src/commands/check-all-with-keywords.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,27 @@ export const check_all_with_keywords_command = (
2020
input_box.title = 'Keyword Search'
2121
input_box.prompt =
2222
'Enter keywords separated by comma (use & for AND, quotes for whole words)'
23-
input_box.placeholder = 'e.g. user, login&auth, "ignoreFocus"'
23+
input_box.placeholder = 'e.g. username, login&auth, "changePassword"'
24+
input_box.ignoreFocusOut = true
25+
26+
const close_button = {
27+
iconPath: new vscode.ThemeIcon('close'),
28+
tooltip: 'Close'
29+
}
30+
input_box.buttons = [close_button]
2431

2532
const keywords_input = await new Promise<string | undefined>(
2633
(resolve) => {
2734
let is_resolved = false
2835
const disposables: vscode.Disposable[] = []
2936

3037
disposables.push(
38+
input_box.onDidTriggerButton((button) => {
39+
if (button === close_button) {
40+
resolve(undefined)
41+
input_box.hide()
42+
}
43+
}),
3144
input_box.onDidAccept(() => {
3245
const value = input_box.value.trim()
3346
if (value.length == 0) {
@@ -215,6 +228,7 @@ export const check_all_with_keywords_command = (
215228
const quick_pick_apply = vscode.window.createQuickPick()
216229
quick_pick_apply.items = quick_pick_options
217230
quick_pick_apply.placeholder = `How would you like to apply the ${selected_paths.length} selected files?`
231+
quick_pick_apply.ignoreFocusOut = true
218232

219233
if (last_choice_label) {
220234
const active_item = quick_pick_options.find(

‎apps/editor/src/utils/apply-reasoning-effort.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@ export const apply_reasoning_effort = (
1414
}
1515
}
1616
} else if (provider.name == 'Google') {
17-
if (reasoning_effort) {
17+
if (reasoning_effort == 'none') {
1818
body.reasoning_effort = reasoning_effort
19-
}
20-
21-
if (reasoning_effort && reasoning_effort != 'none') {
19+
} else {
2220
body.extra_body = {
2321
google: {
2422
thinking_config: {
23+
thinking_level: reasoning_effort,
2524
include_thoughts: true
2625
}
2726
}

‎apps/editor/src/views/panel/backend/message-handlers/handle-code-completion.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from '../utils/replace-git-symbols'
2727
import { replace_saved_context_symbol } from '@/views/panel/backend/utils/replace-saved-context-symbol'
2828
import { replace_skill_symbol } from '../utils/replace-skill-symbol'
29+
import { replace_image_symbol } from '../utils/replace-image-symbol'
2930

3031
const get_code_completion_config = async (
3132
api_providers_manager: ModelProvidersManager,
@@ -404,6 +405,12 @@ export const handle_code_completion = async (
404405
skill_definitions += result.skill_definitions
405406
}
406407

408+
if (processed_completion_instructions.includes('#Image(')) {
409+
processed_completion_instructions = await replace_image_symbol({
410+
instruction: processed_completion_instructions
411+
})
412+
}
413+
407414
const files_collector = new FilesCollector(
408415
panel_provider.workspace_provider,
409416
panel_provider.open_editors_provider
@@ -424,10 +431,33 @@ export const handle_code_completion = async (
424431
: '<missing_text>'
425432
}${payload.after}\n${main_instructions}`
426433

434+
let user_content: any = content
435+
436+
if (content.includes('<cwc-image>')) {
437+
user_content = []
438+
const parts = content.split(/<cwc-image>([\s\S]*?)<\/cwc-image>/)
439+
440+
for (let i = 0; i < parts.length; i++) {
441+
const part = parts[i]
442+
if (i % 2 == 0) {
443+
if (part.length > 0) {
444+
user_content.push({ type: 'text', text: part.trim() })
445+
}
446+
} else {
447+
user_content.push({
448+
type: 'image_url',
449+
image_url: {
450+
url: `data:image/png;base64,${part}`
451+
}
452+
})
453+
}
454+
}
455+
}
456+
427457
const messages = [
428458
{
429459
role: 'user',
430-
content
460+
content: user_content
431461
}
432462
]
433463

‎apps/editor/src/views/panel/backend/message-handlers/handle-copy-prompt.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '@/views/panel/backend/utils/replace-git-symbols'
1010
import { replace_saved_context_symbol } from '@/views/panel/backend/utils/replace-saved-context-symbol'
1111
import { replace_skill_symbol } from '@/views/panel/backend/utils/replace-skill-symbol'
12+
import { replace_image_symbol } from '@/views/panel/backend/utils/replace-image-symbol'
1213
import {
1314
code_at_cursor_instructions_for_panel,
1415
prune_context_instructions_prefix,
@@ -140,6 +141,13 @@ export const handle_copy_prompt = async (params: {
140141
skill_definitions += result.skill_definitions
141142
}
142143

144+
if (processed_completion_instructions.includes('#Image(')) {
145+
processed_completion_instructions = await replace_image_symbol({
146+
instruction: processed_completion_instructions,
147+
remove: true
148+
})
149+
}
150+
143151
const missing_text_tag = processed_completion_instructions
144152
? `<missing_text>${processed_completion_instructions}</missing_text>`
145153
: '<missing_text>'
@@ -205,6 +213,13 @@ export const handle_copy_prompt = async (params: {
205213
skill_definitions += result.skill_definitions
206214
}
207215

216+
if (processed_instructions.includes('#Image(')) {
217+
processed_instructions = await replace_image_symbol({
218+
instruction: processed_instructions,
219+
remove: true
220+
})
221+
}
222+
208223
let system_instructions_xml = ''
209224

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

‎apps/editor/src/views/panel/backend/message-handlers/handle-edit-context.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { replace_saved_context_symbol } from '@/views/panel/backend/utils/replac
2424
import { replace_selection_symbol } from '@/views/panel/backend/utils/replace-selection-symbol'
2525
import { PanelProvider } from '@/views/panel/backend/panel-provider'
2626
import { replace_skill_symbol } from '@/views/panel/backend/utils/replace-skill-symbol'
27+
import { replace_image_symbol } from '@/views/panel/backend/utils/replace-image-symbol'
2728
import { apply_reasoning_effort } from '@/utils/apply-reasoning-effort'
2829
import { EditContextMessage } from '@/views/panel/types/messages'
2930
import { dictionary } from '@shared/constants/dictionary'
@@ -364,6 +365,12 @@ export const handle_edit_context = async (
364365
skill_definitions += result.skill_definitions
365366
}
366367

368+
if (processed_instructions.includes('#Image(')) {
369+
processed_instructions = await replace_image_symbol({
370+
instruction: processed_instructions
371+
})
372+
}
373+
367374
const is_prune_context = panel_provider.api_prompt_type == 'prune-context'
368375
const collected_files = await files_collector.collect_files({
369376
compact: is_prune_context
@@ -465,6 +472,29 @@ export const handle_edit_context = async (
465472
.get<string>('editContextSystemInstructions') ||
466473
default_system_instructions
467474

475+
let user_content: any = content
476+
477+
if (content.includes('<cwc-image>')) {
478+
user_content = []
479+
const parts = content.split(/<cwc-image>([\s\S]*?)<\/cwc-image>/)
480+
481+
for (let i = 0; i < parts.length; i++) {
482+
const part = parts[i]
483+
if (i % 2 == 0) {
484+
if (part.length > 0) {
485+
user_content.push({ type: 'text', text: part.trim() })
486+
}
487+
} else {
488+
user_content.push({
489+
type: 'image_url',
490+
image_url: {
491+
url: `data:image/png;base64,${part}`
492+
}
493+
})
494+
}
495+
}
496+
}
497+
468498
const messages = [
469499
...(system_instructions
470500
? [
@@ -476,7 +506,7 @@ export const handle_edit_context = async (
476506
: []),
477507
{
478508
role: 'user',
479-
content
509+
content: user_content
480510
}
481511
]
482512

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as vscode from 'vscode'
2+
import * as os from 'os'
3+
import * as path from 'path'
4+
import * as fs from 'fs'
5+
import { OpenTempImageMessage } from '../../types/messages'
6+
7+
export const handle_open_temp_image = async (message: OpenTempImageMessage) => {
8+
try {
9+
const hash = message.hash
10+
const txt_filename = `cwc-image-${hash}.txt`
11+
const txt_path = path.join(os.tmpdir(), txt_filename)
12+
13+
// Read base64 content
14+
let content_base64: string
15+
try {
16+
content_base64 = await fs.promises.readFile(txt_path, 'utf-8')
17+
} catch (err: any) {
18+
if (err.code == 'ENOENT') {
19+
vscode.window.showErrorMessage(
20+
'Image file not found (it may have been deleted).'
21+
)
22+
return
23+
}
24+
throw err
25+
}
26+
27+
const buffer = Buffer.from(content_base64, 'base64')
28+
29+
// Create a png file for viewing
30+
const png_filename = `cwc-image-${hash}.png`
31+
const png_path = path.join(os.tmpdir(), png_filename)
32+
33+
await fs.promises.writeFile(png_path, buffer)
34+
35+
// Open the image
36+
const uri = vscode.Uri.file(png_path)
37+
await vscode.commands.executeCommand('vscode.open', uri)
38+
} catch (error) {
39+
vscode.window.showErrorMessage(`Failed to open image: ${error}`)
40+
}
41+
}

‎apps/editor/src/views/panel/backend/message-handlers/handle-preview-preset.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@/views/panel/backend/utils/replace-git-symbols'
1212
import { Preset } from '@shared/types/preset'
1313
import { replace_skill_symbol } from '@/views/panel/backend/utils/replace-skill-symbol'
14+
import { replace_image_symbol } from '@/views/panel/backend/utils/replace-image-symbol'
1415
import { apply_preset_affixes_to_instruction } from '@/utils/apply-preset-affixes'
1516
import { dictionary } from '@shared/constants/dictionary'
1617
import {
@@ -120,6 +121,13 @@ export const handle_preview_preset = async (
120121
skill_definitions += result.skill_definitions
121122
}
122123

124+
if (processed_completion_instructions.includes('#Image(')) {
125+
processed_completion_instructions = await replace_image_symbol({
126+
instruction: processed_completion_instructions,
127+
remove: true
128+
})
129+
}
130+
123131
const missing_text_tag = processed_completion_instructions
124132
? `<missing_text>${processed_completion_instructions}</missing_text>`
125133
: '<missing_text>'
@@ -194,6 +202,13 @@ export const handle_preview_preset = async (
194202
skill_definitions += result.skill_definitions
195203
}
196204

205+
if (processed_instructions.includes('#Image(')) {
206+
processed_instructions = await replace_image_symbol({
207+
instruction: processed_instructions,
208+
remove: true
209+
})
210+
}
211+
197212
let system_instructions_xml = ''
198213
if (panel_provider.web_prompt_type == 'edit-context') {
199214
const config = vscode.workspace.getConfiguration('codeWebChat')

‎apps/editor/src/views/panel/backend/message-handlers/handle-prune-context.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { replace_saved_context_symbol } from '@/views/panel/backend/utils/replac
2222
import { replace_selection_symbol } from '@/views/panel/backend/utils/replace-selection-symbol'
2323
import { PanelProvider } from '@/views/panel/backend/panel-provider'
2424
import { replace_skill_symbol } from '@/views/panel/backend/utils/replace-skill-symbol'
25+
import { replace_image_symbol } from '@/views/panel/backend/utils/replace-image-symbol'
2526
import { apply_reasoning_effort } from '@/utils/apply-reasoning-effort'
2627
import { PruneContextMessage } from '@/views/panel/types/messages'
2728
import { dictionary } from '@shared/constants/dictionary'
@@ -345,6 +346,12 @@ export const handle_prune_context = async (
345346
skill_definitions += result.skill_definitions
346347
}
347348

349+
if (processed_instructions.includes('#Image(')) {
350+
processed_instructions = await replace_image_symbol({
351+
instruction: processed_instructions
352+
})
353+
}
354+
348355
const collected_files = await files_collector.collect_files({
349356
compact: true
350357
})
@@ -409,10 +416,33 @@ export const handle_prune_context = async (
409416

410417
const content = `${system_instructions_xml}\n${skill_definitions}${files}\n${system_instructions_xml}\n${processed_instructions}`
411418

419+
let user_content: any = content
420+
421+
if (content.includes('<cwc-image>')) {
422+
user_content = []
423+
const parts = content.split(/<cwc-image>([\s\S]*?)<\/cwc-image>/)
424+
425+
for (let i = 0; i < parts.length; i++) {
426+
const part = parts[i]
427+
if (i % 2 == 0) {
428+
if (part.length > 0) {
429+
user_content.push({ type: 'text', text: part.trim() })
430+
}
431+
} else {
432+
user_content.push({
433+
type: 'image_url',
434+
image_url: {
435+
url: `data:image/png;base64,${part}`
436+
}
437+
})
438+
}
439+
}
440+
}
441+
412442
const messages = [
413443
{
414444
role: 'user',
415-
content
445+
content: user_content
416446
}
417447
]
418448

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as vscode from 'vscode'
2+
import * as os from 'os'
3+
import * as path from 'path'
4+
import * as fs from 'fs'
5+
import * as crypto from 'crypto'
6+
import { SaveTempImageMessage } from '../../types/messages'
7+
import { PanelProvider } from '../panel-provider'
8+
9+
export const handle_save_temp_image = async (
10+
panel_provider: PanelProvider,
11+
message: SaveTempImageMessage
12+
) => {
13+
try {
14+
const buffer = Buffer.from(message.content_base64, 'base64')
15+
const hash = crypto.createHash('md5').update(buffer).digest('hex')
16+
const filename = `cwc-image-${hash}.txt`
17+
const file_path = path.join(os.tmpdir(), filename)
18+
await fs.promises.writeFile(file_path, message.content_base64, 'utf-8')
19+
panel_provider.add_text_at_cursor_position(`#Image(${hash})`)
20+
} catch (error) {
21+
vscode.window.showErrorMessage(`Failed to save image: ${error}`)
22+
}
23+
}

0 commit comments

Comments
 (0)