From 636512d362c826ea7f7518c6c606ad140521b3fc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 23 Jun 2026 11:45:53 +0000
Subject: [PATCH 01/20] Initial plan
From e0dbd94bab4de570e4a11aaa4558d2a7f93476af Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 23 Jun 2026 11:55:52 +0000
Subject: [PATCH 02/20] Add restart device and check updates commands to Device
View
---
src/commands/VscodeCommand.ts | 2 +
src/managers/RtaManager.ts | 7 ++
src/viewProviders/BaseRdbViewProvider.ts | 26 +++++-
.../RokuDeviceViewViewProvider.ts | 91 +++++++++++++++++-
src/viewProviders/ViewProviderCommand.ts | 4 +-
.../RokuDeviceView/RokuDeviceView.svelte | 93 +++++++++++++++++++
6 files changed, 218 insertions(+), 5 deletions(-)
diff --git a/src/commands/VscodeCommand.ts b/src/commands/VscodeCommand.ts
index 2a2d6fe04..2f9932afe 100644
--- a/src/commands/VscodeCommand.ts
+++ b/src/commands/VscodeCommand.ts
@@ -5,6 +5,8 @@ export enum VscodeCommand {
rokuDeviceViewCopyScreenshot = 'extension.brightscript.rokuDeviceView.copyScreenshot',
rokuDeviceViewEnableNodeInspector = 'extension.brightscript.rokuDeviceView.enableNodeInspector',
rokuDeviceViewDisableNodeInspector = 'extension.brightscript.rokuDeviceView.disableNodeInspector',
+ rokuDeviceViewRestartDevice = 'extension.brightscript.rokuDeviceView.restartDevice',
+ rokuDeviceViewCheckAndInstallUpdates = 'extension.brightscript.rokuDeviceView.checkAndInstallUpdates',
rokuRegistryExportRegistry = 'extension.brightscript.rokuRegistry.exportRegistry',
rokuRegistryImportRegistry = 'extension.brightscript.rokuRegistry.importRegistry',
rokuRegistryClearRegistry = 'extension.brightscript.rokuRegistry.clearRegistry',
diff --git a/src/managers/RtaManager.ts b/src/managers/RtaManager.ts
index ec51e39e3..9feba22af 100644
--- a/src/managers/RtaManager.ts
+++ b/src/managers/RtaManager.ts
@@ -22,11 +22,18 @@ export class RtaManager {
public onDeviceComponent?: rta.OnDeviceComponent;
public device?: rta.RokuDevice;
+ public deviceConfig?: { host: string; password: string };
private webviewViewProviderManager?: WebviewViewProviderManager;
private lastAppUIResponse: rta.AppUIResponse | undefined;
public setupRtaWithConfig(config: { host: string; password: string; logLevel?: string; disableScreenSaver?: boolean; injectRdbOnDeviceComponent?: boolean }) {
+ // Store device config for later use
+ this.deviceConfig = {
+ host: config.host,
+ password: config.password
+ };
+
const enableDebugging = ['info', 'debug', 'trace'].includes(config.logLevel);
const rtaConfig: rta.ConfigOptions = {
RokuDevice: {
diff --git a/src/viewProviders/BaseRdbViewProvider.ts b/src/viewProviders/BaseRdbViewProvider.ts
index 2bce56e82..b6d60fb5e 100644
--- a/src/viewProviders/BaseRdbViewProvider.ts
+++ b/src/viewProviders/BaseRdbViewProvider.ts
@@ -3,6 +3,7 @@ import type * as vscode from 'vscode';
import type { RequestType } from 'roku-test-automation';
import * as fsExtra from 'fs-extra';
import * as path from 'path';
+import { rokuDeploy } from 'roku-deploy';
import { BaseWebviewViewProvider } from './BaseWebviewViewProvider';
import { ViewProviderEvent } from './ViewProviderEvent';
@@ -21,10 +22,29 @@ export abstract class BaseRdbViewProvider extends BaseWebviewViewProvider {
this.setupCommandObservers();
}
- public updateDeviceAvailability() {
+ public async updateDeviceAvailability() {
+ const device = this.dependencies.rtaManager.device;
+ const deviceConfig = this.dependencies.rtaManager.deviceConfig;
+ let deviceInfo = null;
+
+ // Try to get device info if device is available
+ if (device && deviceConfig) {
+ try {
+ const info = await rokuDeploy.getDeviceInfo({
+ host: deviceConfig.host,
+ timeout: 5000
+ });
+ deviceInfo = info;
+ } catch (e) {
+ // Device might be temporarily unavailable, just continue without device info
+ console.error('Failed to get device info:', e);
+ }
+ }
+
const message = this.createEventMessage(ViewProviderEvent.onDeviceAvailabilityChange, {
odcAvailable: !!this.dependencies.rtaManager.onDeviceComponent,
- deviceAvailable: !!this.dependencies.rtaManager.device
+ deviceAvailable: !!device,
+ deviceInfo: deviceInfo
});
this.postOrQueueMessage(message);
@@ -73,6 +93,6 @@ export abstract class BaseRdbViewProvider extends BaseWebviewViewProvider {
protected onViewReady() {
// Always post back the device status so we make sure the client doesn't miss it if it got refreshed
- this.updateDeviceAvailability();
+ void this.updateDeviceAvailability();
}
}
diff --git a/src/viewProviders/RokuDeviceViewViewProvider.ts b/src/viewProviders/RokuDeviceViewViewProvider.ts
index 1460fa8ef..8035546b6 100644
--- a/src/viewProviders/RokuDeviceViewViewProvider.ts
+++ b/src/viewProviders/RokuDeviceViewViewProvider.ts
@@ -1,5 +1,6 @@
-import type * as vscode from 'vscode';
+import * as vscode from 'vscode';
import type { ChannelPublishedEvent } from 'roku-debug';
+import { rokuDeploy } from 'roku-deploy';
import { VscodeCommand } from '../commands/VscodeCommand';
import { BaseRdbViewProvider } from './BaseRdbViewProvider';
import { ViewProviderId } from './ViewProviderId';
@@ -45,6 +46,94 @@ export class RokuDeviceViewViewProvider extends BaseRdbViewProvider {
}
return true;
});
+
+ this.addMessageCommandCallback(ViewProviderCommand.restartDevice, async (message) => {
+ try {
+ const confirm = await vscode.window.showWarningMessage(
+ 'Are you sure you want to restart this device? This will close all running channels.',
+ { modal: true },
+ 'Restart'
+ );
+
+ if (confirm !== 'Restart') {
+ this.postOrQueueMessage(this.createResponseMessage(message, {
+ success: false,
+ cancelled: true
+ }));
+ return true;
+ }
+
+ const device = this.dependencies.rtaManager.device;
+ const deviceConfig = this.dependencies.rtaManager.deviceConfig;
+
+ if (!device || !deviceConfig) {
+ throw new Error('No device connected');
+ }
+
+ await rokuDeploy.rebootDevice({
+ host: deviceConfig.host,
+ password: deviceConfig.password,
+ timeout: 10000
+ });
+
+ this.postOrQueueMessage(this.createResponseMessage(message, {
+ success: true
+ }));
+
+ void vscode.window.showInformationMessage('Device restart initiated successfully');
+ } catch (e) {
+ this.postOrQueueMessage(this.createResponseMessage(message, {
+ success: false,
+ error: e.message
+ }));
+ void vscode.window.showErrorMessage(`Failed to restart device: ${e.message}`);
+ }
+ return true;
+ });
+
+ this.addMessageCommandCallback(ViewProviderCommand.checkForUpdates, async (message) => {
+ try {
+ const confirm = await vscode.window.showInformationMessage(
+ 'Check for software updates on this device? The device will check for and install any available updates.',
+ { modal: true },
+ 'Check for Updates'
+ );
+
+ if (confirm !== 'Check for Updates') {
+ this.postOrQueueMessage(this.createResponseMessage(message, {
+ success: false,
+ cancelled: true
+ }));
+ return true;
+ }
+
+ const device = this.dependencies.rtaManager.device;
+ const deviceConfig = this.dependencies.rtaManager.deviceConfig;
+
+ if (!device || !deviceConfig) {
+ throw new Error('No device connected');
+ }
+
+ await rokuDeploy.checkForUpdate({
+ host: deviceConfig.host,
+ password: deviceConfig.password,
+ timeout: 10000
+ });
+
+ this.postOrQueueMessage(this.createResponseMessage(message, {
+ success: true
+ }));
+
+ void vscode.window.showInformationMessage('Software update check initiated successfully');
+ } catch (e) {
+ this.postOrQueueMessage(this.createResponseMessage(message, {
+ success: false,
+ error: e.message
+ }));
+ void vscode.window.showErrorMessage(`Failed to check for updates: ${e.message}`);
+ }
+ return true;
+ });
}
public onDidStartDebugSession(e: vscode.DebugSession) {
diff --git a/src/viewProviders/ViewProviderCommand.ts b/src/viewProviders/ViewProviderCommand.ts
index a8e9aacfa..8e8a9ee1d 100644
--- a/src/viewProviders/ViewProviderCommand.ts
+++ b/src/viewProviders/ViewProviderCommand.ts
@@ -19,5 +19,7 @@ export enum ViewProviderCommand {
exportRokuAutomationConfigs = 'exportRokuAutomationConfigs',
loadRokuAutomationConfigs = 'loadRokuAutomationConfigs',
updateWorkspaceState = 'updateWorkspaceState',
- viewReady = 'viewReady'
+ viewReady = 'viewReady',
+ restartDevice = 'restartDevice',
+ checkForUpdates = 'checkForUpdates'
}
diff --git a/webviews/src/views/RokuDeviceView/RokuDeviceView.svelte b/webviews/src/views/RokuDeviceView/RokuDeviceView.svelte
index 91398a6d3..f5acd3731 100644
--- a/webviews/src/views/RokuDeviceView/RokuDeviceView.svelte
+++ b/webviews/src/views/RokuDeviceView/RokuDeviceView.svelte
@@ -13,8 +13,77 @@
window.vscode = acquireVsCodeApi();
let deviceAvailable = false;
+ let deviceInfo: any = null;
+
+ // Helper function to compare semantic versions
+ function semverGte(version: string, minVersion: string): boolean {
+ if (!version) return false;
+
+ const parseVersion = (v: string) => {
+ const parts = v.split('.').map(p => parseInt(p, 10));
+ return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
+ };
+
+ const v1 = parseVersion(version);
+ const v2 = parseVersion(minVersion);
+
+ if (v1.major !== v2.major) return v1.major > v2.major;
+ if (v1.minor !== v2.minor) return v1.minor > v2.minor;
+ return v1.patch >= v2.patch;
+ }
+
+ $: softwareVersion = deviceInfo?.['software-version'] || deviceInfo?.softwareVersion || '';
+ $: supportsDeviceActions = semverGte(softwareVersion, '15.0.4');
+ $: deviceActionTooltip = supportsDeviceActions
+ ? ''
+ : 'Requires software version 15.0.4 or later';
+
+ let isRestarting = false;
+ let isCheckingUpdates = false;
+
+ async function onRestartDevice() {
+ if (!supportsDeviceActions || isRestarting) return;
+
+ isRestarting = true;
+ try {
+ const response = await intermediary.sendCommand(ViewProviderCommand.restartDevice);
+ if (!response.cancelled) {
+ // Device will restart, so reset the flag after a delay
+ setTimeout(() => {
+ isRestarting = false;
+ }, 3000);
+ } else {
+ isRestarting = false;
+ }
+ } catch (e) {
+ console.error('Error restarting device:', e);
+ isRestarting = false;
+ }
+ }
+
+ async function onCheckForUpdates() {
+ if (!supportsDeviceActions || isCheckingUpdates) return;
+
+ isCheckingUpdates = true;
+ try {
+ const response = await intermediary.sendCommand(ViewProviderCommand.checkForUpdates);
+ if (!response.cancelled) {
+ // Update check initiated, reset flag after a delay
+ setTimeout(() => {
+ isCheckingUpdates = false;
+ }, 3000);
+ } else {
+ isCheckingUpdates = false;
+ }
+ } catch (e) {
+ console.error('Error checking for updates:', e);
+ isCheckingUpdates = false;
+ }
+ }
+
intermediary.observeEvent(ViewProviderEvent.onDeviceAvailabilityChange, (message) => {
deviceAvailable = message.context.deviceAvailable;
+ deviceInfo = message.context.deviceInfo;
requestScreenshot();
});
@@ -377,6 +446,30 @@