Skip to content

Commit 4bf440e

Browse files
authored
Merge pull request #49 from RoopikLabs/Canvas-Agent-integration
Canvas agent integration + Additional component framework support
2 parents 82d0578 + 4a39029 commit 4bf440e

26 files changed

Lines changed: 1633 additions & 568 deletions

File tree

eslint.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,11 @@ export default tseslint.config(
14531453
'@modelcontextprotocol/sdk',
14541454
'@modelcontextprotocol/sdk/server/mcp.js',
14551455
'@modelcontextprotocol/sdk/server/sse.js',
1456+
// Build tools for Roopik component transformation
1457+
'esbuild',
1458+
'esbuild-svelte',
1459+
'esbuild-plugin-vue3',
1460+
'esbuild-plugin-solid',
14561461
'@parcel/watcher',
14571462
'@vscode/sqlite3',
14581463
'@vscode/vscode-languagedetection',

extensions/roopik-roo/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@
157157
"title": "%command.newTask.title%",
158158
"category": "%configuration.title%"
159159
},
160+
{
161+
"command": "roodio.externalContext",
162+
"title": "Add External Context to Dio",
163+
"category": "%configuration.title%"
164+
},
160165
{
161166
"command": "roodio.terminalAddToContext",
162167
"title": "%command.terminal.addToContext.title%",

extensions/roopik-roo/packages/types/src/mode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ When building components for canvas preview, follow these rules:
184184
- Include ALL code in one file: component logic, styles (CSS-in-JS or inline), types
185185
- For React: Use functional components with hooks, export as default
186186
- For Vue/Svelte: Single-file components work directly
187+
- Dependency Resolution Instruction: The Roopik IDE Canvas (component) build system automatically resolves bare specifier imports (e.g., 'framer-motion', '@headlessui/react') via an internal ESM resolver. You MUST NOT use full CDN URLs (like esm.sh). Always use standard bare imports. The system handles version syncing and cross-dependency conflicts automatically.
187188
188189
Example React component structure:
189190
\`\`\`tsx

extensions/roopik-roo/packages/types/src/vscode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const commandIds = [
4444
"handleHumanRelayResponse",
4545

4646
"newTask",
47+
"externalContext",
4748

4849
"setCustomStoragePath",
4950
"importSettings",

extensions/roopik-roo/src/activate/registerCommands.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,34 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
151151
unregisterHumanRelayCallback: unregisterHumanRelayCallback,
152152
handleHumanRelayResponse: handleHumanRelayResponse,
153153
newTask: handleNewTask,
154+
externalContext: async (payload?: { promptText?: string; images?: string[]; autoSend?: boolean }) => {
155+
const promptText = payload?.promptText?.trim();
156+
if (!promptText) {
157+
return;
158+
}
159+
160+
const provider = await ClineProvider.getInstance();
161+
if (!provider) {
162+
return;
163+
}
164+
165+
if (payload?.autoSend) {
166+
await provider.postMessageToWebview({
167+
type: "invoke",
168+
invoke: "sendMessage",
169+
text: `${promptText}\n\n`,
170+
images: payload?.images,
171+
});
172+
} else {
173+
await provider.postMessageToWebview({
174+
type: "invoke",
175+
invoke: "setChatBoxMessage",
176+
text: `${promptText}\n\n`,
177+
images: payload?.images,
178+
});
179+
await provider.postMessageToWebview({ type: "action", action: "focusInput" });
180+
}
181+
},
154182
setCustomStoragePath: async () => {
155183
const { promptForCustomStoragePath } = await import("../utils/storage")
156184
await promptForCustomStoragePath()

extensions/roopik-roo/src/extension.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,19 @@ export async function activate(context: vscode.ExtensionContext) {
293293

294294
// Pass context to webview if provided
295295
if (options?.message || options?.code) {
296+
const parts: string[] = []
297+
if (options.message) {
298+
parts.push(options.message)
299+
}
300+
if (options.code) {
301+
parts.push(options.code)
302+
}
296303
chatPanelProvider.postMessageToWebview({
297-
type: "openWithContext",
298-
text: options.message,
299-
selectedText: options.code,
304+
type: "invoke",
305+
invoke: "setChatBoxMessage",
306+
text: `${parts.join("\n\n")}\n\n`,
300307
})
308+
chatPanelProvider.postMessageToWebview({ type: "action", action: "focusInput" })
301309
}
302310
}),
303311
)

extensions/roopik/src/canvasPanel.ts

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,41 @@ export class CanvasPanel implements vscode.Disposable {
441441
this.handleWebviewError(message.payload);
442442
break;
443443

444+
case 'canvasAiChat':
445+
await this.handleCanvasAiChat(message.payload as {
446+
userInput: string;
447+
images?: string[];
448+
imageMetadata?: { deviceMode: string; deviceViewport: { width: number; height: number } };
449+
context: {
450+
canvasId: string;
451+
canvasName?: string;
452+
componentCount?: number;
453+
components: Array<{
454+
id: string;
455+
name?: string;
456+
folderPath?: string;
457+
entryFile?: string;
458+
}>;
459+
selectedComponent?: {
460+
id: string;
461+
name?: string;
462+
folderPath?: string;
463+
entryFile?: string;
464+
};
465+
selectedElement?: {
466+
componentId: string;
467+
sourceLocation?: {
468+
file: string;
469+
startLine: number;
470+
endLine?: number;
471+
column?: number;
472+
};
473+
};
474+
};
475+
autoSend?: boolean;
476+
});
477+
break;
478+
444479
default:
445480
this.logger.warn(`Unknown webview message type: ${message.type}`);
446481
}
@@ -452,6 +487,102 @@ export class CanvasPanel implements vscode.Disposable {
452487
}
453488
}
454489

490+
private async handleCanvasAiChat(payload: {
491+
userInput: string;
492+
images?: string[];
493+
imageMetadata?: { deviceMode: string; deviceViewport: { width: number; height: number } };
494+
context: {
495+
canvasId: string;
496+
canvasName?: string;
497+
componentCount?: number;
498+
components: Array<{
499+
id: string;
500+
name?: string;
501+
folderPath?: string;
502+
entryFile?: string;
503+
}>;
504+
selectedComponent?: {
505+
id: string;
506+
name?: string;
507+
folderPath?: string;
508+
entryFile?: string;
509+
};
510+
selectedElement?: {
511+
componentId: string;
512+
sourceLocation?: {
513+
file: string;
514+
startLine: number;
515+
endLine?: number;
516+
column?: number;
517+
};
518+
};
519+
};
520+
autoSend?: boolean;
521+
}): Promise<void> {
522+
const userInput = payload?.userInput?.trim() ?? '';
523+
const images = payload?.images;
524+
const imageMetadata = payload?.imageMetadata;
525+
if (!userInput && (!images || images.length === 0)) {
526+
return;
527+
}
528+
529+
const context = payload.context;
530+
const lines: string[] = [];
531+
lines.push('[Roopik Canvas Context]');
532+
lines.push(`canvasId: ${context.canvasId}`);
533+
if (context.canvasName) {
534+
lines.push(`canvasName: ${context.canvasName}`);
535+
}
536+
537+
if (context.selectedComponent) {
538+
lines.push('Selected component:');
539+
lines.push(`- id: ${context.selectedComponent.id}`);
540+
if (context.selectedComponent.name) {
541+
lines.push(`- name: ${context.selectedComponent.name}`);
542+
}
543+
if (context.selectedComponent.folderPath) {
544+
lines.push(`- folderPath: ${context.selectedComponent.folderPath}`);
545+
}
546+
if (context.selectedComponent.entryFile) {
547+
const entryPath = context.selectedComponent.folderPath
548+
? path.join(context.selectedComponent.folderPath, context.selectedComponent.entryFile)
549+
: context.selectedComponent.entryFile;
550+
lines.push(`- entryFile: ${entryPath}`);
551+
}
552+
}
553+
554+
if (context.selectedElement?.sourceLocation) {
555+
const source = context.selectedElement.sourceLocation;
556+
const lineRange = source.endLine && source.endLine !== source.startLine
557+
? `${source.startLine}-${source.endLine}`
558+
: `${source.startLine}`;
559+
lines.push('Selected element:');
560+
lines.push(`- componentId: ${context.selectedElement.componentId}`);
561+
lines.push(`- source: ${source.file}:${lineRange}`);
562+
}
563+
564+
lines.push('');
565+
if (images && images.length > 0 && imageMetadata) {
566+
lines.push(`Device mode: ${imageMetadata.deviceMode}`);
567+
lines.push(`Device viewport: ${imageMetadata.deviceViewport.width}x${imageMetadata.deviceViewport.height}`);
568+
}
569+
570+
if (userInput) {
571+
lines.push('User request:');
572+
lines.push(userInput);
573+
}
574+
575+
try {
576+
await vscode.commands.executeCommand('roodio.externalContext', {
577+
promptText: lines.join('\n'),
578+
images,
579+
autoSend: payload.autoSend === true,
580+
});
581+
} catch (error) {
582+
this.logger.error(`Failed to forward canvas AI context: ${error instanceof Error ? error.message : String(error)}`);
583+
}
584+
}
585+
455586
/**
456587
* Handle drop component request from webview (drag-drop from OS file manager)
457588
*
@@ -669,7 +800,9 @@ export class CanvasPanel implements vscode.Disposable {
669800
this.loadedComponents.push({
670801
componentId,
671802
contentHash: ref.contentHash,
672-
name: ref.componentName
803+
name: ref.componentName,
804+
folderPath: ref.folderPath,
805+
entryFile: ref.entryFile
673806
});
674807
}
675808
}
@@ -720,13 +853,15 @@ export class CanvasPanel implements vscode.Disposable {
720853
this.logger.info(`Loading ${this.loadedComponents.length} existing components`);
721854

722855
for (const component of this.loadedComponents) {
723-
const { componentId, contentHash, name } = component;
856+
const { componentId, contentHash, name, folderPath, entryFile } = component;
724857

725858
// 1. Notify webview that component exists (shows loading spinner)
726859
this.postToWebview('componentCreated', {
727860
componentId,
728861
canvasId: this.canvasId,
729-
name
862+
name,
863+
folderPath,
864+
entryFile
730865
});
731866

732867
// 2. Load component (checks cache, rebuilds if needed)

extensions/roopik/src/componentLoader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export interface ComponentLoadInfo {
6363
componentId: string;
6464
contentHash: string;
6565
name?: string;
66+
folderPath?: string;
67+
entryFile?: string;
6668
}
6769

6870
// ============================================================================

extensions/roopik/src/extension.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ export async function activate(context: vscode.ExtensionContext) {
5454
try {
5555
await manager.initialize(context);
5656
logger.info('Extension', 'RoopikExtensionManager initialized');
57+
58+
// Restore last active canvas (if any)
59+
try {
60+
// const restored = await manager.restoreLastActiveCanvas(context.extensionUri);
61+
await manager.restoreLastActiveCanvas(context.extensionUri);
62+
// if (restored) {
63+
// logger.info('Extension', 'Last active canvas restored successfully');
64+
// }
65+
} catch (error) {
66+
// Don't fail activation if restoration fails
67+
logger.warn('Extension', `Failed to restore last active canvas: ${error}`);
68+
}
5769
} catch (error) {
5870
logger.error('Extension', 'Failed to initialize RoopikExtensionManager', error);
5971
vscode.window.showErrorMessage(`Roopik initialization failed: ${error}`);

0 commit comments

Comments
 (0)