Skip to content

Commit c7b2921

Browse files
mybarmancodex
andcommitted
fix: use VS Code terminal paste for image paths
Prefer VS Code's terminal paste command over direct sendText so uploaded image paths reach Claude/Codex TUIs again in remote sessions. AI-assisted by Codex. Co-Authored-By: Codex <codex@openai.com>
1 parent 3baabe0 commit c7b2921

6 files changed

Lines changed: 299 additions & 30 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "claudeboard",
3-
"displayName": "Claudeboard",
3+
"displayName": "Claudeboard Local",
44
"description": "Share images with Claude Code running on a remote server via Remote-SSH",
5-
"version": "1.0.1",
5+
"version": "1.0.5",
66
"publisher": "dkodr",
77
"author": "Dariusz Kuśnierek",
88
"license": "MIT",
@@ -128,4 +128,4 @@
128128
"typescript": "^5.0.0"
129129
},
130130
"dependencies": {}
131-
}
131+
}

src/commands/uploadImage.ts

Lines changed: 155 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ClipboardService, ImageData } from '../services/clipboard';
33
import { FileManagerService, ImageFile } from '../services/fileManager';
44
import { ProgressService, ProgressPatterns, ProgressSteps } from '../services/progress';
55
import { ConfigurationService } from '../services/configuration';
6+
import { Logger } from '../services/logging';
67
import { Result, success, failure, ExtensionResult, ClipboardError, FileSystemError } from '../common/result';
78

89
export type InsertDestination = 'editor' | 'terminal';
@@ -16,15 +17,78 @@ export interface CommandDependencies {
1617
fileManager: FileManagerService;
1718
progress: ProgressService;
1819
config: ConfigurationService;
20+
logger: Logger;
21+
}
22+
23+
function delay(ms: number): Promise<void> {
24+
return new Promise(resolve => setTimeout(resolve, ms));
25+
}
26+
27+
function asBracketedPaste(text: string): string {
28+
return `\x1b[200~${text}\x1b[201~`;
29+
}
30+
31+
async function pasteTextIntoActiveTerminal(text: string, logger: Logger): Promise<ExtensionResult<void>> {
32+
const activeTerminal = vscode.window.activeTerminal;
33+
if (!activeTerminal) {
34+
logger.error('No active terminal available for insertion');
35+
return failure(new FileSystemError('No active terminal available'));
36+
}
37+
38+
try {
39+
logger.info('Attempting terminal insertion', {
40+
textLength: text.length,
41+
terminalName: activeTerminal.name
42+
});
43+
activeTerminal.show(false);
44+
await vscode.commands.executeCommand('workbench.action.terminal.focus');
45+
await delay(100);
46+
await vscode.env.clipboard.writeText(text);
47+
await vscode.commands.executeCommand('workbench.action.terminal.paste');
48+
logger.info('VS Code terminal paste command completed', {
49+
textLength: text.length
50+
});
51+
vscode.window.showInformationMessage('Claudeboard Local: attempted VS Code terminal paste command');
52+
return success(undefined);
53+
} catch (pasteCommandError) {
54+
logger.warn('VS Code terminal paste command failed', { pasteCommandError });
55+
try {
56+
activeTerminal.sendText(asBracketedPaste(text), false);
57+
logger.warn('Bracketed paste sendText fallback completed', {
58+
textLength: text.length
59+
});
60+
vscode.window.showWarningMessage('Claudeboard Local: terminal paste command failed; used bracketed sendText fallback');
61+
return success(undefined);
62+
} catch (bracketedPasteError) {
63+
await vscode.env.clipboard.writeText(text);
64+
logger.error('Terminal paste and bracketed sendText both failed', {
65+
pasteCommandError,
66+
bracketedPasteError,
67+
textLength: text.length
68+
});
69+
return failure(new FileSystemError(
70+
'Failed to insert image URL into terminal. The path was copied to the clipboard; press Cmd+V/Ctrl+V in the terminal.',
71+
{ pasteCommandError, bracketedPasteError, text }
72+
));
73+
}
74+
}
1975
}
2076

2177
class ImageUploadCommand implements UploadImageCommand {
2278
constructor(private readonly deps: CommandDependencies) {}
2379

2480
async execute(destination: InsertDestination): Promise<ExtensionResult<string>> {
81+
this.deps.logger.info('Upload command started', {
82+
destination,
83+
remoteName: vscode.env.remoteName
84+
});
2585
// Validate remote connection first
2686
const remoteCheck = this.validateRemoteConnection();
2787
if (Result.isFailure(remoteCheck)) {
88+
this.deps.logger.error('Remote validation failed', {
89+
destination,
90+
error: remoteCheck.error
91+
});
2892
return remoteCheck;
2993
}
3094

@@ -49,14 +113,25 @@ class ImageUploadCommand implements UploadImageCommand {
49113

50114
private async checkClipboard(): Promise<ExtensionResult<ImageData>> {
51115
try {
116+
const hasImage = await this.deps.clipboard.hasImage().catch(error => {
117+
this.deps.logger.debug('hasImage probe failed before getImage', { error });
118+
return false;
119+
});
120+
this.deps.logger.info('Clipboard probe before getImage', { hasImage });
52121
const imageData = await this.deps.clipboard.getImage();
53122

54123
if (!imageData) {
124+
this.deps.logger.warn('getImage returned null');
55125
return failure(new ClipboardError('No image found in clipboard'));
56126
}
57127

128+
this.deps.logger.info('Clipboard image retrieved', {
129+
format: imageData.format,
130+
bytes: imageData.buffer.length
131+
});
58132
return success(imageData);
59133
} catch (error) {
134+
this.deps.logger.error('Clipboard access failed', { error });
60135
return failure(new ClipboardError(
61136
'Failed to access clipboard',
62137
{ originalError: error }
@@ -69,17 +144,20 @@ class ImageUploadCommand implements UploadImageCommand {
69144
reporter: any
70145
): Promise<ExtensionResult<string>> {
71146
reporter.report(ProgressSteps.preparing());
147+
this.deps.logger.debug('Upload and insert start', { destination });
72148

73149
try {
74150
// Get image from previous step's result - this is a simplified approach
75151
// In a more complex implementation, we'd pass results between steps
76152
const imageData = await this.deps.clipboard.getImage();
77153
if (!imageData) {
154+
this.deps.logger.warn('Image disappeared between clipboard check and upload');
78155
return failure(new ClipboardError('Image no longer available in clipboard'));
79156
}
80157

81158
// Cleanup old images based on user configuration
82159
const retentionDays = this.deps.config.getRetentionDays();
160+
this.deps.logger.debug('Cleaning old images', { retentionDays });
83161
await this.deps.fileManager.cleanupOldImages(retentionDays);
84162

85163
reporter.report(ProgressSteps.uploading());
@@ -89,12 +167,22 @@ class ImageUploadCommand implements UploadImageCommand {
89167
imageData.buffer,
90168
imageData.format
91169
);
170+
this.deps.logger.info('Image file created', {
171+
imagePath: imageFile.getPath(),
172+
format: imageData.format,
173+
bytes: imageData.buffer.length
174+
});
92175

93176
reporter.report(ProgressSteps.inserting());
94177

95178
// Insert URL into editor/terminal
96179
const insertResult = await this.insertImageUrl(imageFile.getPath(), destination);
97180
if (Result.isFailure(insertResult)) {
181+
this.deps.logger.error('Insertion failed after file creation', {
182+
destination,
183+
imagePath: imageFile.getPath(),
184+
error: insertResult.error
185+
});
98186
imageFile.dispose();
99187
return insertResult;
100188
}
@@ -104,16 +192,22 @@ class ImageUploadCommand implements UploadImageCommand {
104192
// Clear clipboard if configured to do so
105193
if (this.deps.config.getClearClipboardAfterUpload()) {
106194
await this.deps.clipboard.clear();
195+
this.deps.logger.debug('Clipboard cleared after upload');
107196
}
108197

109198
const imageUrl = imageFile.getPath();
110199

111200
// Show success message
112201
vscode.window.showInformationMessage(`Image uploaded: ${imageUrl}`);
202+
this.deps.logger.info('Upload command completed successfully', {
203+
destination,
204+
imageUrl
205+
});
113206

114207
return success(imageUrl);
115208

116209
} catch (error) {
210+
this.deps.logger.error('Upload and insert threw', { error, destination });
117211
return failure(new FileSystemError(
118212
'Failed to upload image',
119213
{ originalError: error, destination }
@@ -133,13 +227,9 @@ class ImageUploadCommand implements UploadImageCommand {
133227
await activeEditor.edit(editBuilder => {
134228
editBuilder.insert(position, url);
135229
});
230+
this.deps.logger.info('Inserted image path into editor', { url });
136231
} else if (destination === 'terminal') {
137-
const activeTerminal = vscode.window.activeTerminal;
138-
if (!activeTerminal) {
139-
return failure(new FileSystemError('No active terminal available'));
140-
}
141-
142-
activeTerminal.sendText(url, false);
232+
return await pasteTextIntoActiveTerminal(url, this.deps.logger);
143233
}
144234

145235
return success(undefined);
@@ -157,9 +247,17 @@ class OptimizedImageUploadCommand implements UploadImageCommand {
157247
constructor(private readonly deps: CommandDependencies) {}
158248

159249
async execute(destination: InsertDestination): Promise<ExtensionResult<string>> {
250+
this.deps.logger.info('Optimized upload command started', {
251+
destination,
252+
remoteName: vscode.env.remoteName
253+
});
160254
// Validate remote connection first
161255
const remoteCheck = this.validateRemoteConnection();
162256
if (Result.isFailure(remoteCheck)) {
257+
this.deps.logger.error('Remote validation failed', {
258+
destination,
259+
error: remoteCheck.error
260+
});
163261
return remoteCheck;
164262
}
165263

@@ -170,6 +268,10 @@ class OptimizedImageUploadCommand implements UploadImageCommand {
170268
);
171269

172270
if (Result.isFailure(clipboardResult)) {
271+
this.deps.logger.warn('Clipboard check failed in optimized command', {
272+
destination,
273+
error: clipboardResult.error
274+
});
173275
vscode.window.showWarningMessage(clipboardResult.error.message);
174276
return clipboardResult;
175277
}
@@ -193,14 +295,25 @@ class OptimizedImageUploadCommand implements UploadImageCommand {
193295

194296
private async checkClipboard(): Promise<ExtensionResult<ImageData>> {
195297
try {
298+
const hasImage = await this.deps.clipboard.hasImage().catch(error => {
299+
this.deps.logger.debug('hasImage probe failed before getImage', { error });
300+
return false;
301+
});
302+
this.deps.logger.info('Clipboard probe before getImage', { hasImage });
196303
const imageData = await this.deps.clipboard.getImage();
197304

198305
if (!imageData) {
306+
this.deps.logger.warn('getImage returned null');
199307
return failure(new ClipboardError('No image found in clipboard'));
200308
}
201309

310+
this.deps.logger.info('Clipboard image retrieved', {
311+
format: imageData.format,
312+
bytes: imageData.buffer.length
313+
});
202314
return success(imageData);
203315
} catch (error) {
316+
this.deps.logger.error('Clipboard access failed', { error });
204317
return failure(new ClipboardError(
205318
'Failed to access clipboard',
206319
{ originalError: error }
@@ -215,9 +328,15 @@ class OptimizedImageUploadCommand implements UploadImageCommand {
215328
): Promise<ExtensionResult<string>> {
216329
try {
217330
reporter.report(ProgressSteps.preparing());
331+
this.deps.logger.debug('Optimized upload and insert start', {
332+
destination,
333+
format: imageData.format,
334+
bytes: imageData.buffer.length
335+
});
218336

219337
// Cleanup old images first based on user configuration
220338
const retentionDays = this.deps.config.getRetentionDays();
339+
this.deps.logger.debug('Cleaning old images', { retentionDays });
221340
await this.deps.fileManager.cleanupOldImages(retentionDays);
222341

223342
reporter.report(ProgressSteps.uploading());
@@ -227,12 +346,22 @@ class OptimizedImageUploadCommand implements UploadImageCommand {
227346
imageData.buffer,
228347
imageData.format
229348
);
349+
this.deps.logger.info('Image file created', {
350+
imagePath: imageFile.getPath(),
351+
format: imageData.format,
352+
bytes: imageData.buffer.length
353+
});
230354

231355
reporter.report(ProgressSteps.inserting());
232356

233357
// Insert URL into editor/terminal
234358
const insertResult = await this.insertImageUrl(imageFile.getPath(), destination);
235359
if (Result.isFailure(insertResult)) {
360+
this.deps.logger.error('Insertion failed after file creation', {
361+
destination,
362+
imagePath: imageFile.getPath(),
363+
error: insertResult.error
364+
});
236365
imageFile.dispose();
237366
return insertResult;
238367
}
@@ -242,16 +371,22 @@ class OptimizedImageUploadCommand implements UploadImageCommand {
242371
// Clear clipboard if configured to do so
243372
if (this.deps.config.getClearClipboardAfterUpload()) {
244373
await this.deps.clipboard.clear();
374+
this.deps.logger.debug('Clipboard cleared after upload');
245375
}
246376

247377
const imageUrl = imageFile.getPath();
248378

249379
// Show success message
250380
vscode.window.showInformationMessage(`Image uploaded: ${imageUrl}`);
381+
this.deps.logger.info('Optimized upload command completed successfully', {
382+
destination,
383+
imageUrl
384+
});
251385

252386
return success(imageUrl);
253387

254388
} catch (error) {
389+
this.deps.logger.error('Optimized upload and insert threw', { error, destination });
255390
return failure(new FileSystemError(
256391
'Failed to upload image',
257392
{ originalError: error, destination }
@@ -271,13 +406,9 @@ class OptimizedImageUploadCommand implements UploadImageCommand {
271406
await activeEditor.edit(editBuilder => {
272407
editBuilder.insert(position, url);
273408
});
409+
this.deps.logger.info('Inserted image path into editor', { url });
274410
} else if (destination === 'terminal') {
275-
const activeTerminal = vscode.window.activeTerminal;
276-
if (!activeTerminal) {
277-
return failure(new FileSystemError('No active terminal available'));
278-
}
279-
280-
activeTerminal.sendText(url, false);
411+
return await pasteTextIntoActiveTerminal(url, this.deps.logger);
281412
}
282413

283414
return success(undefined);
@@ -304,6 +435,17 @@ export async function handleUploadCommand(
304435
const result = await command.execute(destination);
305436

306437
if (Result.isFailure(result)) {
438+
deps.logger.error('Command handler returning failure', {
439+
destination,
440+
error: result.error
441+
});
442+
deps.logger.show(true);
307443
vscode.window.showErrorMessage(`Upload error: ${result.error.message}`);
444+
return;
308445
}
309-
}
446+
447+
deps.logger.info('Command handler returning success', {
448+
destination,
449+
imageUrl: result.data
450+
});
451+
}

0 commit comments

Comments
 (0)