Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,11 @@ export default tseslint.config(
'@modelcontextprotocol/sdk',
'@modelcontextprotocol/sdk/server/mcp.js',
'@modelcontextprotocol/sdk/server/sse.js',
// Build tools for Roopik component transformation
'esbuild',
'esbuild-svelte',
'esbuild-plugin-vue3',
'esbuild-plugin-solid',
'@parcel/watcher',
'@vscode/sqlite3',
'@vscode/vscode-languagedetection',
Expand Down
5 changes: 5 additions & 0 deletions extensions/roopik-roo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
"title": "%command.newTask.title%",
"category": "%configuration.title%"
},
{
"command": "roodio.externalContext",
"title": "Add External Context to Dio",
"category": "%configuration.title%"
},
{
"command": "roodio.terminalAddToContext",
"title": "%command.terminal.addToContext.title%",
Expand Down
1 change: 1 addition & 0 deletions extensions/roopik-roo/packages/types/src/mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ When building components for canvas preview, follow these rules:
- Include ALL code in one file: component logic, styles (CSS-in-JS or inline), types
- For React: Use functional components with hooks, export as default
- For Vue/Svelte: Single-file components work directly
- 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.

Example React component structure:
\`\`\`tsx
Expand Down
1 change: 1 addition & 0 deletions extensions/roopik-roo/packages/types/src/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const commandIds = [
"handleHumanRelayResponse",

"newTask",
"externalContext",

"setCustomStoragePath",
"importSettings",
Expand Down
28 changes: 28 additions & 0 deletions extensions/roopik-roo/src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,34 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
unregisterHumanRelayCallback: unregisterHumanRelayCallback,
handleHumanRelayResponse: handleHumanRelayResponse,
newTask: handleNewTask,
externalContext: async (payload?: { promptText?: string; images?: string[]; autoSend?: boolean }) => {
const promptText = payload?.promptText?.trim();
if (!promptText) {
return;
}

const provider = await ClineProvider.getInstance();
if (!provider) {
return;
}

if (payload?.autoSend) {
await provider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: `${promptText}\n\n`,
images: payload?.images,
});
} else {
await provider.postMessageToWebview({
type: "invoke",
invoke: "setChatBoxMessage",
text: `${promptText}\n\n`,
images: payload?.images,
});
await provider.postMessageToWebview({ type: "action", action: "focusInput" });
}
},
setCustomStoragePath: async () => {
const { promptForCustomStoragePath } = await import("../utils/storage")
await promptForCustomStoragePath()
Expand Down
14 changes: 11 additions & 3 deletions extensions/roopik-roo/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,19 @@ export async function activate(context: vscode.ExtensionContext) {

// Pass context to webview if provided
if (options?.message || options?.code) {
const parts: string[] = []
if (options.message) {
parts.push(options.message)
}
if (options.code) {
parts.push(options.code)
}
chatPanelProvider.postMessageToWebview({
type: "openWithContext",
text: options.message,
selectedText: options.code,
type: "invoke",
invoke: "setChatBoxMessage",
text: `${parts.join("\n\n")}\n\n`,
})
chatPanelProvider.postMessageToWebview({ type: "action", action: "focusInput" })
}
}),
)
Expand Down
141 changes: 138 additions & 3 deletions extensions/roopik/src/canvasPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,41 @@ export class CanvasPanel implements vscode.Disposable {
this.handleWebviewError(message.payload);
break;

case 'canvasAiChat':
await this.handleCanvasAiChat(message.payload as {
userInput: string;
images?: string[];
imageMetadata?: { deviceMode: string; deviceViewport: { width: number; height: number } };
context: {
canvasId: string;
canvasName?: string;
componentCount?: number;
components: Array<{
id: string;
name?: string;
folderPath?: string;
entryFile?: string;
}>;
selectedComponent?: {
id: string;
name?: string;
folderPath?: string;
entryFile?: string;
};
selectedElement?: {
componentId: string;
sourceLocation?: {
file: string;
startLine: number;
endLine?: number;
column?: number;
};
};
};
autoSend?: boolean;
});
break;

default:
this.logger.warn(`Unknown webview message type: ${message.type}`);
}
Expand All @@ -452,6 +487,102 @@ export class CanvasPanel implements vscode.Disposable {
}
}

private async handleCanvasAiChat(payload: {
userInput: string;
images?: string[];
imageMetadata?: { deviceMode: string; deviceViewport: { width: number; height: number } };
context: {
canvasId: string;
canvasName?: string;
componentCount?: number;
components: Array<{
id: string;
name?: string;
folderPath?: string;
entryFile?: string;
}>;
selectedComponent?: {
id: string;
name?: string;
folderPath?: string;
entryFile?: string;
};
selectedElement?: {
componentId: string;
sourceLocation?: {
file: string;
startLine: number;
endLine?: number;
column?: number;
};
};
};
autoSend?: boolean;
}): Promise<void> {
const userInput = payload?.userInput?.trim() ?? '';
const images = payload?.images;
const imageMetadata = payload?.imageMetadata;
if (!userInput && (!images || images.length === 0)) {
return;
}

const context = payload.context;
const lines: string[] = [];
lines.push('[Roopik Canvas Context]');
lines.push(`canvasId: ${context.canvasId}`);
if (context.canvasName) {
lines.push(`canvasName: ${context.canvasName}`);
}

if (context.selectedComponent) {
lines.push('Selected component:');
lines.push(`- id: ${context.selectedComponent.id}`);
if (context.selectedComponent.name) {
lines.push(`- name: ${context.selectedComponent.name}`);
}
if (context.selectedComponent.folderPath) {
lines.push(`- folderPath: ${context.selectedComponent.folderPath}`);
}
if (context.selectedComponent.entryFile) {
const entryPath = context.selectedComponent.folderPath
? path.join(context.selectedComponent.folderPath, context.selectedComponent.entryFile)
: context.selectedComponent.entryFile;
lines.push(`- entryFile: ${entryPath}`);
}
}

if (context.selectedElement?.sourceLocation) {
const source = context.selectedElement.sourceLocation;
const lineRange = source.endLine && source.endLine !== source.startLine
? `${source.startLine}-${source.endLine}`
: `${source.startLine}`;
lines.push('Selected element:');
lines.push(`- componentId: ${context.selectedElement.componentId}`);
lines.push(`- source: ${source.file}:${lineRange}`);
}

lines.push('');
if (images && images.length > 0 && imageMetadata) {
lines.push(`Device mode: ${imageMetadata.deviceMode}`);
lines.push(`Device viewport: ${imageMetadata.deviceViewport.width}x${imageMetadata.deviceViewport.height}`);
}

if (userInput) {
lines.push('User request:');
lines.push(userInput);
}

try {
await vscode.commands.executeCommand('roodio.externalContext', {
promptText: lines.join('\n'),
images,
autoSend: payload.autoSend === true,
});
} catch (error) {
this.logger.error(`Failed to forward canvas AI context: ${error instanceof Error ? error.message : String(error)}`);
}
}

/**
* Handle drop component request from webview (drag-drop from OS file manager)
*
Expand Down Expand Up @@ -669,7 +800,9 @@ export class CanvasPanel implements vscode.Disposable {
this.loadedComponents.push({
componentId,
contentHash: ref.contentHash,
name: ref.componentName
name: ref.componentName,
folderPath: ref.folderPath,
entryFile: ref.entryFile
});
}
}
Expand Down Expand Up @@ -720,13 +853,15 @@ export class CanvasPanel implements vscode.Disposable {
this.logger.info(`Loading ${this.loadedComponents.length} existing components`);

for (const component of this.loadedComponents) {
const { componentId, contentHash, name } = component;
const { componentId, contentHash, name, folderPath, entryFile } = component;

// 1. Notify webview that component exists (shows loading spinner)
this.postToWebview('componentCreated', {
componentId,
canvasId: this.canvasId,
name
name,
folderPath,
entryFile
});

// 2. Load component (checks cache, rebuilds if needed)
Expand Down
2 changes: 2 additions & 0 deletions extensions/roopik/src/componentLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface ComponentLoadInfo {
componentId: string;
contentHash: string;
name?: string;
folderPath?: string;
entryFile?: string;
}

// ============================================================================
Expand Down
12 changes: 12 additions & 0 deletions extensions/roopik/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ export async function activate(context: vscode.ExtensionContext) {
try {
await manager.initialize(context);
logger.info('Extension', 'RoopikExtensionManager initialized');

// Restore last active canvas (if any)
try {
// const restored = await manager.restoreLastActiveCanvas(context.extensionUri);
await manager.restoreLastActiveCanvas(context.extensionUri);
// if (restored) {
// logger.info('Extension', 'Last active canvas restored successfully');
// }
} catch (error) {
// Don't fail activation if restoration fails
logger.warn('Extension', `Failed to restore last active canvas: ${error}`);
}
} catch (error) {
logger.error('Extension', 'Failed to initialize RoopikExtensionManager', error);
vscode.window.showErrorMessage(`Roopik initialization failed: ${error}`);
Expand Down
Loading
Loading