Skip to content
Open
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 apps/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,11 @@
},
"views": {
"rocketride": [
{
"type": "webview",
"id": "rocketride.provider.connection",
"name": "Connections"
},
{
"type": "webview",
"id": "rocketride.sidebar.main",
Expand Down
1 change: 1 addition & 0 deletions apps/vscode/rsbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default defineConfig({
'page-account': './src/providers/views/PageAccount/index.tsx',
'page-billing': './src/providers/views/PageBilling/index.tsx',
'page-auth': './src/providers/views/PageAuth/index.tsx',
'page-connection': './src/providers/views/PageConnection/index.tsx',
},
},

Expand Down
47 changes: 43 additions & 4 deletions apps/vscode/src/connection/local-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,17 @@ export class LocalManager extends BaseManager {
let attempts = 0;

while (attempts < MAX_CONNECT_ATTEMPTS) {
if (token?.isCancellationRequested) {
this.logger.output(`${icons.warning} Connection cancelled by user`);
throw new Error('Connection cancelled');
}
Comment on lines +86 to +89
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== LocalManager cancellation throws ==="
rg -n "Connection cancelled|CancellationError" apps/vscode/src/connection/local-manager.ts -C2

echo
echo "=== Existing CancellationError handling patterns ==="
rg -n "instanceof\\s+vscode\\.CancellationError" apps/vscode/src -C2

Repository: rocketride-org/rocketride-server

Length of output: 1830


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking LocalManager file for cancellation error patterns ==="
# First, locate and display the specific lines mentioned
if [ -f "apps/vscode/src/connection/local-manager.ts" ]; then
  echo "File found. Checking lines 86-89:"
  sed -n '86,89p' apps/vscode/src/connection/local-manager.ts || true
  echo
  echo "Checking lines 149-150:"
  sed -n '149,150p' apps/vscode/src/connection/local-manager.ts || true
  echo
  echo "Checking lines 168-169:"
  sed -n '168,169p' apps/vscode/src/connection/local-manager.ts || true
  echo
  echo "=== All 'Connection cancelled' references in local-manager.ts ==="
  rg -n "Connection cancelled" apps/vscode/src/connection/local-manager.ts || true
else
  echo "File not found at expected location"
fi

echo
echo "=== Checking for CancellationError usage patterns in vscode connection code ==="
rg -n "CancellationError" apps/vscode/src/connection/ -t ts || true

echo
echo "=== Checking imports in local-manager.ts ==="
head -20 apps/vscode/src/connection/local-manager.ts || true

Repository: rocketride-org/rocketride-server

Length of output: 2528


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking vscode import in local-manager.ts ==="
grep -n "import.*vscode" apps/vscode/src/connection/local-manager.ts || echo "No vscode import found in first pass"

echo
echo "=== Full import section of local-manager.ts ==="
sed -n '1,50p' apps/vscode/src/connection/local-manager.ts | grep -E "^import|^from" || true

echo
echo "=== Check for vscode usage elsewhere in local-manager.ts ==="
rg -n "vscode\." apps/vscode/src/connection/local-manager.ts | head -10 || true

Repository: rocketride-org/rocketride-server

Length of output: 869


Standardize cancellation exceptions to vscode.CancellationError.

Lines 88, 149, and 168 use generic Error('Connection cancelled'), while engine-manager.ts checks instanceof vscode.CancellationError for cancellation handling. Throw vscode.CancellationError() instead so cancellation exceptions remain distinguishable from real failures across connection flows.

♻️ Proposed change
-				throw new Error('Connection cancelled');
+				throw new vscode.CancellationError();
...
-				return reject(new Error('Connection cancelled'));
+				return reject(new vscode.CancellationError());
...
-				reject(new Error('Connection cancelled'));
+				reject(new vscode.CancellationError());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/vscode/src/connection/local-manager.ts` around lines 86 - 89, Replace
the generic throws of Error('Connection cancelled') in local-manager.ts with
vscode.CancellationError so cancellation flows are recognized by
engine-manager.ts; specifically update the throw in the
token?.isCancellationRequested branch (and the other two places that currently
throw Error('Connection cancelled')) to throw new vscode.CancellationError(),
and add/import the vscode namespace if not already imported so the symbol
resolves.


try {
await client.connect(LOCAL_AUTH, { uri, timeout: 5000 });
await client.connect({
redirectUri: uri,
auth: LOCAL_AUTH,
timeout: 5000
} as any);
return;
} catch (error: unknown) {
attempts++;
Expand All @@ -99,7 +108,9 @@ export class LocalManager extends BaseManager {
const delaySec = Math.round(delayMs / 1000);
this.logger.output(`${icons.info} Connection attempt ${attempts} failed, waiting ${delaySec}s...`);
this.emit('status', `Waiting ${delaySec}s before next attempt`);
await LocalManager.delay(delayMs);

// Cancellable delay
await this.delayWithToken(delayMs, token);
}
}
}
Expand Down Expand Up @@ -136,7 +147,35 @@ export class LocalManager extends BaseManager {
return Math.min(delay, BACKOFF_MAX_MS);
}

private static delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
private delayWithToken(ms: number, token?: vscode.CancellationToken): Promise<void> {
return new Promise((resolve, reject) => {
if (token?.isCancellationRequested) {
return reject(new Error('Connection cancelled'));
}

let timeout: NodeJS.Timeout | undefined;
let sub: vscode.Disposable | undefined;

const cleanup = () => {
if (timeout !== undefined) {
clearTimeout(timeout);
timeout = undefined;
}
if (sub !== undefined) {
sub.dispose();
sub = undefined;
}
};

sub = token?.onCancellationRequested(() => {
cleanup();
reject(new Error('Connection cancelled'));
});

timeout = setTimeout(() => {
cleanup();
resolve();
}, ms);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
7 changes: 7 additions & 0 deletions apps/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { PageAuthProvider } from './providers/PageAuthProvider';
import { AgentManager } from './agents/agent-manager';
import { syncServiceCatalog } from './agents/services';
import { CloudAuthProvider } from './auth/CloudAuthProvider';
import { PageConnectionProvider } from './providers/PageConnectionProvider';

// Core managers
let connectionManager: ConnectionManager | undefined;
Expand Down Expand Up @@ -228,6 +229,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
logger.output(`${icons.info} Creating tree providers...`);
progress.report({ increment: 50, message: 'Creating tree providers...' });

//-------------------------------------
//
//-------------------------------------
const connectionProvider = new PageConnectionProvider(context.extensionUri);
context.subscriptions.push(vscode.window.registerWebviewViewProvider(PageConnectionProvider.viewType, connectionProvider));

// Register unified sidebar webview
pageSidebar = new PageSidebarProvider(context.extensionUri);
const sidebarWebviewProvider = vscode.window.registerWebviewViewProvider(PageSidebarProvider.viewType, pageSidebar);
Expand Down
254 changes: 254 additions & 0 deletions apps/vscode/src/providers/PageConnectionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// =============================================================================
// MIT License
// Copyright (c) 2026 Aparavi Software AG
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// =============================================================================

/**
* Connection Webview Provider for Connection Management
*
* Provides a rich webview interface for connection management with:
* - Real-time connection status updates
* - Visual connection state indicators
* - Interactive connection controls
* - Configuration status display
* - Settings access
*/

import * as vscode from 'vscode';
import { RocketRideClient } from 'rocketride';
import { ConfigManager } from '../config';
import { ConnectionManager } from '../connection/connection';

export class PageConnectionProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'rocketride.provider.connection';

private _view?: vscode.WebviewView;
private disposables: vscode.Disposable[] = [];
private configManager = ConfigManager.getInstance();
private connectionManager = ConnectionManager.getInstance();

constructor(private readonly extensionUri: vscode.Uri) {
this.setupEventListeners();
}

/**
* Resolves the webview view
*/
public resolveWebviewView(
webviewView: vscode.WebviewView,
_context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
this._view = webviewView;

webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [this.extensionUri]
};

webviewView.webview.html = this.getHtmlForWebview(webviewView.webview);

// Handle messages from the webview
const messageDisposable = webviewView.webview.onDidReceiveMessage(async (message) => {
try {
switch (message.type) {
case 'ready':
await this.sendConnectionUpdate();
break;

case 'connect':
await this.connectionManager.connect();
break;

case 'disconnect':
await this.connectionManager.disconnect();
break;

case 'reconnect':
await this.connectionManager.reconnect();
break;

case 'openSettings':
vscode.commands.executeCommand('rocketride.page.settings.open');
break;

case 'openDocs':
vscode.env.openExternal(vscode.Uri.parse('https://docs.rocketride.org'));
break;

case 'openDeploy':
vscode.commands.executeCommand('rocketride.page.deploy.open');
break;

}
} catch (error) {
console.error('[PageConnectionProvider] Message handling error:', error);
}
});

this.disposables.push(messageDisposable);
}

/**
* Sets up event listeners for connection and configuration changes
*/
private setupEventListeners(): void {
// Listen for connection state changes
const connectionStateListener = this.connectionManager.on('connectionStateChanged', () => {
this.sendConnectionUpdate();
});

const connectedListener = this.connectionManager.on('connected', () => {
this.sendConnectionUpdate();
});

const errorListener = this.connectionManager.on('error', () => {
this.sendConnectionUpdate();
});

// Listen for config changes
const configChangeListener = vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('rocketride')) {
this.sendConnectionUpdate();
}
});

this.disposables.push(
connectionStateListener,
connectedListener,
errorListener,
configChangeListener
);
}

/**
* Sends connection status update to webview
*/
private async sendConnectionUpdate(): Promise<void> {
if (!this._view) {
return;
}

try {
const connectionState = this.connectionManager.getConnectionStatus();
const config = this.configManager.getConfig();
const hasApiKey = this.configManager.hasApiKey();
const engineInfo = this.connectionManager.getEngineInfo();

this._view.webview.postMessage({
type: 'connectionUpdate',
data: {
connectionState,
config: {
hostUrl: RocketRideClient.normalizeUri(config.hostUrl),
connectionMode: config.connectionMode,
autoConnect: config.autoConnect
},
hasApiKey,
engineInfo
}
});
} catch (error) {
console.error('[PageConnectionProvider] Failed to send connection update:', error);
}
}

/**
* Generates HTML content for the webview
*/
private getHtmlForWebview(webview: vscode.Webview): string {
const nonce = this.generateNonce();
const htmlPath = vscode.Uri.joinPath(this.extensionUri, 'webview', 'page-connection.html');

try {
let htmlContent = require('fs').readFileSync(htmlPath.fsPath, 'utf8');

// Replace template placeholders
htmlContent = htmlContent
.replace(/\{\{nonce\}\}/g, nonce)
.replace(/\{\{cspSource\}\}/g, webview.cspSource);

// Convert resource URLs to webview URIs
return htmlContent.replace(
/(?:src|href)="(\/static\/[^"]+)"/g,
(match: string, relativePath: string): string => {
const cleanPath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
const resourceUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, 'webview', cleanPath)
);
return match.replace(relativePath, resourceUri.toString());
}
);
} catch (error) {
console.error('Error loading connection HTML:', error);
return this.getErrorHtml(error, htmlPath.fsPath);
}
}

/**
* Generates fallback HTML for when the main HTML file can't be loaded
*/
private getErrorHtml(error: unknown, expectedPath: string): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connection View Error</title>
</head>
<body>
<div style="padding: 20px; color: #f44336;">
<h3>Error Loading Connection View</h3>
<p><strong>Error:</strong> ${error}</p>
<p>Run <code>npm run build:webview</code> to build the webview.</p>
<p>Expected: <code>${expectedPath}</code></p>
</div>
</body>
</html>`;
}

/**
* Generates a random nonce for Content Security Policy
*/
private generateNonce(): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

/**
* Public method to refresh the connection view
*/
public refresh(): void {
this.sendConnectionUpdate();
}

/**
* Cleans up event listeners and resources
*/
public dispose(): void {
this.disposables.forEach(disposable => disposable.dispose());
this.disposables = [];
}
}
Loading
Loading