Skip to content

Add wasm to render console in frontend#163

Merged
LittleLittleCloud merged 19 commits intomainfrom
u/xiaoyun/wasm
Nov 23, 2025
Merged

Add wasm to render console in frontend#163
LittleLittleCloud merged 19 commits intomainfrom
u/xiaoyun/wasm

Conversation

@LittleLittleCloud
Copy link
Member

@LittleLittleCloud LittleLittleCloud commented Nov 22, 2025

#156

Recording.2025-11-23.000044.mp4

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds WebAssembly support to render console output in a frontend application. The implementation introduces a new Blazor WebAssembly project (RazorConsole.Website) with xterm.js integration for terminal rendering in the browser.

Key changes:

  • New Blazor WebAssembly project setup
  • Integration of xterm.js library for terminal emulation
  • JavaScript interop layer for terminal operations
  • Static assets (Bootstrap, fonts, icons, sample data)

Reviewed changes

Copilot reviewed 31 out of 76 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
RazorConsole.Website.csproj New Blazor WebAssembly project targeting .NET 10.0
Properties/launchSettings.json Development server configuration with HTTP/HTTPS profiles
wwwroot/index.html Main HTML entry point with xterm.js and Blazor bootstrap
wwwroot/js/xterm-interop.js JavaScript interop for terminal initialization and I/O
wwwroot/lib/* Third-party libraries (xterm.js, Bootstrap CSS)
wwwroot/sample-data/weather.json Sample weather data fixture
wwwroot/*.png Application icons and favicon
_Imports.razor Global Razor component imports
Pages/NotFound.razor 404 error page component

</div>
<script src="lib/xterm.min.js"></script>
<script src="js/xterm-interop.js"></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The Blazor WebAssembly script URL contains invalid syntax. The fingerprint placeholder format #[.{fingerprint}] is non-standard. It should be either blazor.webassembly.js for development or use the correct MSBuild token syntax like _framework/blazor.webassembly.js (the framework handles fingerprinting automatically during publish).

Suggested change
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
<script src="_framework/blazor.webassembly.js"></script>

Copilot uses AI. Check for mistakes.

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The empty <TargetFrameworks> element should be removed as it serves no purpose and may cause confusion. The singular <TargetFramework> on line 4 is already specified and sufficient.

Suggested change
<TargetFrameworks></TargetFrameworks>

Copilot uses AI. Check for mistakes.
const terminal = getTerminal(elementId);

terminal.onKey(function (event) {
dotnetHelper.invokeMethodAsync("HandleKeyboardEvent", event.key, event.domEvent.key, event.domEvent.ctrlKey, event.domEvent.altKey, event.domEvent.shiftKey);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The event.key parameter is passed twice (once as the first parameter and again as event.domEvent.key). This appears redundant - verify if both are needed or remove the duplicate.

Suggested change
dotnetHelper.invokeMethodAsync("HandleKeyboardEvent", event.key, event.domEvent.key, event.domEvent.ctrlKey, event.domEvent.altKey, event.domEvent.shiftKey);
dotnetHelper.invokeMethodAsync("HandleKeyboardEvent", event.key, event.domEvent.ctrlKey, event.domEvent.altKey, event.domEvent.shiftKey);

Copilot uses AI. Check for mistakes.
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="RazorConsole.Website.styles.css" rel="stylesheet" />
<script type="importmap"></script>
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Empty import map script tag should either be removed or populated with actual import mappings. An empty import map serves no purpose and adds unnecessary DOM elements.

Suggested change
<script type="importmap"></script>

Copilot uses AI. Check for mistakes.
{
throw new InvalidOperationException("The renderer has not been initialized.");
}
var sw = new StringWriter();
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Disposable 'StringWriter' is created but not disposed.

Suggested change
var sw = new StringWriter();
using var sw = new StringWriter();

Copilot uses AI. Check for mistakes.
console.Profile.Width = 80;
console.Profile.Height = 150;

var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150));
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

This assignment to consoleOption is useless, since its value is never read.

Suggested change
var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150));

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +191
var modifiers = ConsoleModifiers.None;
if (ctrlKey)
{
modifiers |= ConsoleModifiers.Control;
}
if (altKey)
{
modifiers |= ConsoleModifiers.Alt;
}
if (shiftKey)
{
modifiers |= ConsoleModifiers.Shift;
}
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

This assignment to modifiers is useless, since its value is never read.

Suggested change
var modifiers = ConsoleModifiers.None;
if (ctrlKey)
{
modifiers |= ConsoleModifiers.Control;
}
if (altKey)
{
modifiers |= ConsoleModifiers.Alt;
}
if (shiftKey)
{
modifiers |= ConsoleModifiers.Shift;
}

Copilot uses AI. Check for mistakes.
@ParadiseFallen
Copy link
Collaborator

ParadiseFallen commented Nov 22, 2025

I was thinking about a bit diffrent approach.

  1. Make RazorConsole compile to wasm
  2. Add interop layer (i.e. JSImport JSExport things)
  3. Run xterm and push/pull events in and out from wasm.

Docs site is still using react and not blazor. We can export blazor components as react (i.e.

builder.RootComponents.RegisterForJavaScript<Quote>(identifier: "quote", 
    javaScriptInitializer: "initializeComponent");

) but im not sure how well it works.

There is a WasmBrowser template in newer net10 which does basically what we need. So need to build and add interop.

I like how you using copilot. can i also ask it to make changes in draft? (and docs whould be nice)

@TeseySTD
Copy link
Member

@ParadiseFallen

Docs site is still using react and not blazor. We can export blazor components as react

Looks very good, but maybe .net runtime in wasm will require some initial time in client, we must find the way to init runtime once in client, and then just run wasm examples.

I like how you using copilot. can i also ask it to make changes in draft? (and docs whould be nice)

You can use copilot in comments to drafts, just summoning him by @copilot

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 52 out of 55 changed files in this pull request and generated 27 comments.

Files not reviewed (1)
  • website/package-lock.json: Language not supported

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>exe</OutputType>
<TargetFrameworks></TargetFrameworks>
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The TargetFrameworks element on line 6 is empty, which might cause build issues. This should either specify target frameworks or be removed entirely since TargetFramework is already defined on line 4.

Copilot uses AI. Check for mistakes.
await blazorStartPromise
}

export async function mountComponentPreview(componentName: string, terminalElementId: string): Promise<() => void> {
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

[nitpick] The function returns () => void but the returned arrow function doesn't have an explicit return statement. While this works in JavaScript, it would be clearer to either type it as Promise<() => void> or document that the returned cleanup function is synchronous.

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +152
console.Profile.Width = 80;
console.Profile.Height = 150;
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Magic numbers detected. The terminal dimensions (80 columns, 150 rows) are hardcoded here as well. These values should be consistent with other parts of the application and ideally centralized in a shared configuration.

Copilot uses AI. Check for mistakes.
}
catch (Exception ex)
{
Console.WriteLine($"Error rendering component: {ex.Message} {ex.StackTrace}");
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Console.WriteLine should not be used for error logging in production code. This error handling also re-throws the exception after logging, which is acceptable, but consider using ILogger for consistent logging throughout the application.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +68
console.log(`Getting existing terminal: ${elementId}`);
const terminal = getTerminalInstance(elementId);
console.log('Terminal found:', terminal);
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Console.WriteLine calls should be removed or replaced with proper logging. These debug statements appear in multiple places throughout the interop code.

Copilot uses AI. Check for mistakes.
{
throw new InvalidOperationException("The renderer has not been initialized.");
}
var sw = new StringWriter();
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Disposable 'StringWriter' is created but not disposed.

Copilot uses AI. Check for mistakes.
console.Profile.Width = 80;
console.Profile.Height = 150;

var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150));
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

This assignment to consoleOption is useless, since its value is never read.

Copilot uses AI. Check for mistakes.
}
if (shiftKey)
{
modifiers |= ConsoleModifiers.Shift;
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

This assignment to modifiers is useless, since its value is never read.

Copilot uses AI. Check for mistakes.
<Markup Content="Use arrow keys to navigate and Enter to select." Foreground="Color.Green" />
</Rows>
@code {
private string[] options = new[] { "Option 1", "Option 2", "Option 3" };
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Field 'options' can be 'readonly'.

Suggested change
private string[] options = new[] { "Option 1", "Option 2", "Option 3" };
private readonly string[] options = new[] { "Option 1", "Option 2", "Option 3" };

Copilot uses AI. Check for mistakes.
ShowLineNumbers="true" />

@code {
private string codeSnippet = @"
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Field 'codeSnippet' can be 'readonly'.

Suggested change
private string codeSnippet = @"
private readonly string codeSnippet = @"

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings November 23, 2025 07:56
@LittleLittleCloud LittleLittleCloud changed the title WIP Add wasm to render console in frontend Add wasm to render console in frontend Nov 23, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated 19 comments.

Files not reviewed (1)
  • website/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

src/RazorConsole.Website/RazorConsoleRenderer.cs:162

  • This assignment to consoleOption is useless, since its value is never read.

@@ -0,0 +1,83 @@
console.log('main.js loaded');
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Debug console.log statement should be removed before merging.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +130
console.log("Terminal host element is not available");
return;
}

const term = new Terminal({
fontFamily: "'Cascadia Code', 'Fira Code', Consolas, 'Courier New', monospace",
fontSize: 14,
lineHeight: 1.2,
cursorBlink: true,
scrollback: 1000,
cursorInactiveStyle: 'none',
theme: isDark ? TERMINAL_THEME.dark : TERMINAL_THEME.light,
allowProposedApi: true
});

xtermRef.current = term;

const disposeSafely = () => {
if (disposed) {
return;
}
disposed = true;
term.dispose();
xtermRef.current = null;
};
console.log("Initializing terminal preview for", elementId);
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Debug console.log statements should be removed before merging.

Copilot uses AI. Check for mistakes.
console.Profile.Width = 80;
console.Profile.Height = 150;

var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150));
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The variable consoleOption is declared but never used. Consider removing it or using it if it was intended for something.

Copilot uses AI. Check for mistakes.

const module = await moduleCache.get(absoluteUrl)!;

console.log(`Loaded .NET module from ${absoluteUrl}`);
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Debug console.log statement should be removed before merging.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +24
<ItemGroup>
<!-- use .net10 ASP.NET -->
<!-- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" VersionOverride="10.0.0" /> -->
<!-- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" VersionOverride="10.0.0" PrivateAssets="all" /> -->
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Commented-out package references should be removed if not needed, or uncommented if they are needed. Leaving commented code reduces maintainability.

Copilot uses AI. Check for mistakes.
public partial class Registry
{
private static readonly Dictionary<string, IRazorConsoleRenderer> _renderers = new();
private static readonly HashSet<string> _subscriptions = new();
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The _subscriptions field is declared but never used. Consider removing it if it's not needed for future functionality.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +34
const previewRegistry: Record<string, PreviewDefinition> = {
Align: {
tagName: 'razor-console-align',
componentType: 'RazorConsole.Website.RazorConsoleComponents.AlignDemo, RazorConsole.Website'
}
}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The previewRegistry only contains a single component 'Align', but the system appears designed to support multiple components based on the structure in Program.cs which has a switch statement for many components (Align, Border, Scrollable, etc.). This registry appears to be incomplete or unused. Consider removing this code if it's not needed, or properly implementing it to match the actual component registration system.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +22
Console.WriteLine("Program.cs loaded");
[SupportedOSPlatform("browser")]
public partial class Registry
{
private static readonly Dictionary<string, IRazorConsoleRenderer> _renderers = new();
private static readonly HashSet<string> _subscriptions = new();

[JSExport]
[SupportedOSPlatform("browser")]
public static void RegisterComponent(string elementID)
{
Console.WriteLine(elementID);
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Debug Console.WriteLine statements should be removed or converted to proper logging before merging.

Copilot uses AI. Check for mistakes.
{
throw new InvalidOperationException("The renderer has not been initialized.");
}
var sw = new StringWriter();
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Disposable 'StringWriter' is created but not disposed.

Copilot uses AI. Check for mistakes.
}
if (shiftKey)
{
modifiers |= ConsoleModifiers.Shift;
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

This assignment to modifiers is useless, since its value is never read.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 64 out of 67 changed files in this pull request and generated 19 comments.

Files not reviewed (1)
  • website/package-lock.json: Language not supported

Comment on lines +66 to +100
console.log(`Getting existing terminal: ${elementId}`);
const terminal = getTerminalInstance(elementId);
console.log('Terminal found:', terminal);
if (!terminal) {
throw new Error(`Terminal with id '${elementId}' has not been initialized.`);
}
return terminal;
}
export function isTerminalAvailable() {
return typeof window !== 'undefined' && typeof window.Terminal === 'function';
}

export function registerTerminalInstance(elementId, terminal) {
terminalInstances.set(elementId, terminal);
}

export function getTerminalInstance(elementId) {
return terminalInstances.get(elementId);
}
/**
* @param {string} elementId
* @param {RazorConsoleTerminalOptions | undefined} options
* @returns {import('xterm').Terminal}
*/
export function initTerminal(elementId, options) {
const TerminalCtor = getTerminalConstructor();
const host = ensureHostElement(elementId);
disposeTerminal(elementId);
const mergedOptions = {
...defaultOptions,
...options,
theme: mergeThemes(defaultOptions.theme, options?.theme)
};

console.log('Merged terminal options:', mergedOptions);
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Console.log statements should not be present in production code. Consider using a proper logging library or removing debug statements before merging.

Debug logs found at lines 38, 66, 68, and 100.

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +174
return () => {
cancelled = true;
if (disposeTimer !== null) {
clearTimeout(disposeTimer);
}
disposeTimer = window.setTimeout(disposeSafely, 0);
};
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The variable disposeTimer is declared but never assigned or used except for cleanup. The setTimeout is called but the result is assigned to disposeTimer which is then immediately checked in the return cleanup. This creates a potential memory leak if the component is unmounted quickly.

Consider simplifying the disposal logic or ensuring the timer is properly tracked:

let disposeTimeoutId: ReturnType<typeof setTimeout> | null = null;
// ...
return () => {
    cancelled = true;
    if (disposeTimeoutId !== null) {
        clearTimeout(disposeTimeoutId);
    }
    disposeTimeoutId = setTimeout(disposeSafely, 0);
};

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +48
const load = async (currentUrl: string): Promise<any> => {
const absoluteUrl = new URL(currentUrl, window.location.origin).toString();

if (!moduleCache.has(absoluteUrl)) {
const modulePromise = (async () => {
const response = await fetch(absoluteUrl, { cache: 'no-cache' });
if (!response.ok) {
throw new Error(`Failed to fetch ${absoluteUrl}: ${response.status} ${response.statusText}`);
}

const source = await response.text();
const blobUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
try {
return await import(/* @vite-ignore */ blobUrl);
} finally {
URL.revokeObjectURL(blobUrl);
}
})().catch(error => {
moduleCache.delete(absoluteUrl);
throw error;
});

moduleCache.set(absoluteUrl, modulePromise);
}

const module = await moduleCache.get(absoluteUrl)!;

console.log(`Loaded .NET module from ${absoluteUrl}`);

const { getAssemblyExports, getConfig } = await module
.dotnet
.withDiagnosticTracing(false)
.create();

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
return exports;
};
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The load function is defined but never called. This appears to be dead code that should be removed or integrated into the component logic.

Copilot uses AI. Check for mistakes.
scrollback: 1000,
cols: 80,
rows: 150,
rendererType: 'dom'
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The rendererType: 'dom' option is deprecated in xterm.js. The renderer type is automatically chosen based on browser capabilities in newer versions.

Consider removing this option to avoid potential issues with future versions of xterm.js.

Copilot uses AI. Check for mistakes.
Comment on lines +254 to +256
public void OnCompleted()
{
return;
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The OnCompleted method has an empty return statement which is unnecessary for a void method. Simply remove the return statement or make the method body empty.

public void OnCompleted()
{
}

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +193
_serviceProvider = services.BuildServiceProvider();
_consoleRenderer = _serviceProvider.GetRequiredService<ConsoleRenderer>();
_keyboardEventManager = _serviceProvider.GetRequiredService<KeyboardEventManager>();
var focusManager = _serviceProvider.GetRequiredService<FocusManager>();

_ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = AnsiSupport.Yes,
ColorSystem = ColorSystemSupport.Standard,
Out = new AnsiConsoleOutput(_sw)
});

_ansiConsole.Profile.Capabilities.Unicode = true;

_ansiConsole.Profile.Width = 80;
_ansiConsole.Profile.Height = 150;
var snapshot = await _consoleRenderer.MountComponentAsync<TComponent>(ParameterView.Empty, default).ConfigureAwait(false);
_consoleRenderer.Subscribe(this);
_consoleRenderer.Subscribe(focusManager);

var initialView = ConsoleViewResult.FromSnapshot(snapshot);
var canvas = new LiveDisplayCanvas(_ansiConsole);
var consoleLiveDisplayContext = new ConsoleLiveDisplayContext(canvas, _consoleRenderer, initialView);
var focusSession = focusManager.BeginSession(consoleLiveDisplayContext, initialView, CancellationToken.None);
await focusSession.InitializationTask.ConfigureAwait(false);
canvas.Refreshed += () =>
{
var output = _sw.ToString();
SnapshotRendered?.Invoke(output);
XTermInterop.WriteToTerminal(_componentId, output);
_sw.GetStringBuilder().Clear();
};
}

/// <summary>
/// Render Razor component to ANSI string that can be sent to xterm.js.
/// </summary>
/// <returns></returns>
public async Task<string> RenderAsync()
{
await EnsureInitializedAsync().ConfigureAwait(false);

if (_consoleRenderer is null)
{
throw new InvalidOperationException("The renderer has not been initialized.");
}
var sw = new StringWriter();
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = AnsiSupport.Yes,
ColorSystem = ColorSystemSupport.Standard,
Out = new AnsiConsoleOutput(sw)
});

console.Profile.Width = 80;
console.Profile.Height = 150;

var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150));

var snapshot = await _consoleRenderer.MountComponentAsync<TComponent>(ParameterView.Empty, CancellationToken.None);

try
{
console.Write(snapshot.Renderable!);
}
catch (Exception ex)
{
Console.WriteLine($"Error rendering component: {ex.Message} {ex.StackTrace}");

throw;
}

return sw.ToString();
}

/// <summary>
/// Processes a keyboard event from the browser.
/// </summary>
public async Task HandleKeyboardEventAsync(string xtermKey, string domKey, bool ctrlKey, bool altKey, bool shiftKey)
{
await EnsureInitializedAsync().ConfigureAwait(false);

if (_keyboardEventManager is null)
{
return;
}

var keyInfo = ParseKeyFromBrowser(xtermKey, domKey, ctrlKey, altKey, shiftKey);
Console.WriteLine($"Parsed ConsoleKeyInfo: KeyChar='{keyInfo.KeyChar}', Key={keyInfo.Key}, Modifiers={keyInfo.Modifiers}");
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Console.log and Console.WriteLine statements should not be present in production code. Consider using a proper logging framework or removing debug statements.

Debug logs found at lines 105, 130, 172, 193.

Copilot uses AI. Check for mistakes.
Comment on lines +277 to +281
catch (Exception ex)
{
Console.WriteLine($"Error rendering component: {ex.Message} {ex.StackTrace}");
throw;
}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The error handling catches all exceptions but only writes a generic message to Console.WriteLine. This makes debugging difficult and the error message is not user-friendly.

Consider:

  1. Using a proper logging framework instead of Console.WriteLine
  2. Providing more specific error messages based on the exception type
  3. Potentially notifying the user through the UI when rendering fails

Copilot uses AI. Check for mistakes.
console.Profile.Width = 80;
console.Profile.Height = 150;

var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150));
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

This assignment to consoleOption is useless, since its value is never read.

Copilot uses AI. Check for mistakes.
<Markup Content="Use arrow keys to navigate and Enter to select." Foreground="Color.Green" />
</Rows>
@code {
private string[] options = new[] { "Option 1", "Option 2", "Option 3" };
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Field 'options' can be 'readonly'.

Copilot uses AI. Check for mistakes.
ShowLineNumbers="true" />

@code {
private string codeSnippet = @"
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Field 'codeSnippet' can be 'readonly'.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings November 23, 2025 17:18
@github-actions
Copy link

github-actions bot commented Nov 23, 2025

🚀 Preview Deployment

A preview build has been generated for this PR!

Download the artifact:
website-preview-pr-163

To view the preview locally:

  1. Download the artifact from the workflow run
  2. Extract the ZIP file
  3. Serve the files with a local web server
    (e.g., npx serve dist)

Live Preview URL: https://f81fef77.razorconsole.pages.dev

The live preview will be automatically updated when you push new
commits to this PR.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 64 out of 67 changed files in this pull request and generated 19 comments.

Files not reviewed (1)
  • website/package-lock.json: Language not supported

Comment on lines +54 to +56
load(url)
.then(exports => setDotNet(exports))
.finally(() => setLoading(false));
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The error handling in the load function catches the error and removes it from moduleCache, but the error is then re-thrown. However, in the useEffect, the promise rejection is not caught with .catch(), which could lead to unhandled promise rejections.

Add error handling in the useEffect:

load(url)
  .then(exports => setDotNet(exports))
  .catch(error => {
    console.error('Failed to load .NET module:', error);
    // Optionally set an error state
  })
  .finally(() => setLoading(false));

Copilot uses AI. Check for mistakes.
Comment on lines +263 to +276
public void OnNext(ConsoleRenderer.RenderSnapshot value)
{
try
{
if (value.Renderable is null)
{
return;
}

var output = _sw.ToString();
SnapshotRendered?.Invoke(output);
XTermInterop.WriteToTerminal(_componentId, output);
_sw.GetStringBuilder().Clear();
}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

In the OnNext method, when rendering the component, the output is retrieved from _sw (the instance's StringWriter), but then a new StringWriter named sw is created in the RenderAsync method but never used in OnNext. This appears to be leftover code.

Additionally, there's a logic issue: the OnNext method should be writing to the _ansiConsole which uses _sw, but it looks like the rendering setup in InitializeAsync should handle the writing. The current implementation in OnNext might not be correctly triggering the rendering pipeline.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +88
switch (elementID)
{
case "Align":
_renderers[elementID] = new RazorConsoleRenderer<Align_1>(elementID);
break;
case "Border":
_renderers[elementID] = new RazorConsoleRenderer<Border_1>(elementID);
break;
case "Scrollable":
_renderers[elementID] = new RazorConsoleRenderer<Scrollable_1>(elementID);
break;
case "Columns":
_renderers[elementID] = new RazorConsoleRenderer<Columns_1>(elementID);
break;
case "Rows":
_renderers[elementID] = new RazorConsoleRenderer<Rows_1>(elementID);
break;
case "Grid":
_renderers[elementID] = new RazorConsoleRenderer<Grid_1>(elementID);
break;
case "Padder":
_renderers[elementID] = new RazorConsoleRenderer<Padder_1>(elementID);
break;
case "TextButton":
_renderers[elementID] = new RazorConsoleRenderer<TextButton_1>(elementID);
break;
case "TextInput":
_renderers[elementID] = new RazorConsoleRenderer<TextInput_1>(elementID);
break;
case "Select":
_renderers[elementID] = new RazorConsoleRenderer<Select_1>(elementID);
break;
case "Markup":
_renderers[elementID] = new RazorConsoleRenderer<Markup_1>(elementID);
break;
case "Markdown":
_renderers[elementID] = new RazorConsoleRenderer<Markdown_1>(elementID);
break;
case "Panel":
_renderers[elementID] = new RazorConsoleRenderer<Panel_1>(elementID);
break;
case "Figlet":
_renderers[elementID] = new RazorConsoleRenderer<Figlet_1>(elementID);
break;
case "SyntaxHighlighter":
_renderers[elementID] = new RazorConsoleRenderer<SyntaxHighlighter_1>(elementID);
break;
case "Table":
_renderers[elementID] = new RazorConsoleRenderer<Table_1>(elementID);
break;
case "Spinner":
_renderers[elementID] = new RazorConsoleRenderer<Spinner_1>(elementID);
break;
case "Newline":
_renderers[elementID] = new RazorConsoleRenderer<Newline_1>(elementID);
break;
case "SpectreCanvas":
_renderers[elementID] = new RazorConsoleRenderer<SpectreCanvas_1>(elementID);
break;
case "BarChart":
_renderers[elementID] = new RazorConsoleRenderer<BarChart_1>(elementID);
break;
case "BreakdownChart":
_renderers[elementID] = new RazorConsoleRenderer<BreakdownChart_1>(elementID);
break;
}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Inconsistent error handling: If an unrecognized elementID is passed to RegisterComponent, the method silently does nothing. This makes debugging difficult since callers won't know why their component didn't register.

Consider logging a warning or throwing an exception:

default:
    Console.WriteLine($"Warning: Unknown component type '{elementID}'");
    // or throw new ArgumentException($"Unknown component type: {elementID}", nameof(elementID));
    break;

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +25
<ItemGroup>
<!-- use .net10 ASP.NET -->
<!-- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" VersionOverride="10.0.0" /> -->
<!-- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" VersionOverride="10.0.0" PrivateAssets="all" /> -->
</ItemGroup>
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

[nitpick] The commented-out package references suggest uncertainty about the required dependencies. These should either be uncommented if needed, or removed entirely to avoid confusion.

If these packages are required for .NET 10 ASP.NET WebAssembly support, they should be uncommented. If not, remove the comments and add a note in documentation about the dependency strategy.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,83 @@
console.log('main.js loaded');
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Console logging should not be left in production code. Multiple console.log statements are present throughout the codebase (e.g., lines 1, 66, 68, 100, 130). These should be removed or replaced with a proper logging mechanism that can be disabled in production.

Consider using a logging utility or removing these debug statements before merging.

Copilot uses AI. Check for mistakes.

if (!moduleCache.has(absoluteUrl)) {
const modulePromise = (async () => {
const response = await fetch(absoluteUrl, { cache: 'no-cache' });
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Security concern: The { cache: 'no-cache' } option in the fetch call disables HTTP caching, but for immutable module code, caching would improve performance.

Consider using a more appropriate cache strategy:

const response = await fetch(absoluteUrl, { 
    cache: 'force-cache' // or 'default' for standard caching behavior
});

If you need fresh content during development but want caching in production, use a build-time environment variable to control this behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +58
useEffect(() => {
if (dotnetUrl.current !== url) { // safeguard to prevent double-loading
setLoading(true);
dotnetUrl.current = url;
load(url)
.then(exports => setDotNet(exports))
.finally(() => setLoading(false));
}
}, [url]);
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The useDotNet hook contains a potential race condition. If the url prop changes rapidly or the component unmounts before the load operation completes, this could lead to setting state on an unmounted component or loading the wrong module.

Consider:

  1. Adding cleanup logic to cancel pending loads when url changes or component unmounts
  2. Using an abort controller or checking if the component is still mounted before calling setDotNet
  3. Clearing the dotnetUrl.current on unmount to properly reset state

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +27
const blobUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
try {
return await import(/* @vite-ignore */ blobUrl);
} finally {
URL.revokeObjectURL(blobUrl);
}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Memory leak: The blob URL created with URL.createObjectURL in the finally block is revoked immediately after import, but if the import fails before reaching the finally block, the blob URL will never be revoked, causing a memory leak.

Consider restructuring to ensure cleanup:

let blobUrl: string | undefined;
try {
  blobUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
  return await import(/* @vite-ignore */ blobUrl);
} finally {
  if (blobUrl) {
    URL.revokeObjectURL(blobUrl);
  }
}

Copilot uses AI. Check for mistakes.
scrollback: 1000,
cols: 80,
rows: 150,
rendererType: 'dom'
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The rendererType option is being set to 'dom', but this is not a valid option according to the xterm.js API. The xterm.js library uses rendererType: 'dom' | 'canvas' in older versions, but in newer versions this option has been removed in favor of automatic selection.

Since xterm 5.x is being used, this property might be ignored or cause issues. Verify if this is necessary and remove it if not supported in the current version.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +22
public static void RegisterComponent(string elementID)
{
Console.WriteLine(elementID);
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Missing null check: The method doesn't validate that elementID is not null or empty before using it as a dictionary key and creating renderers. This could lead to unexpected behavior or exceptions.

Add validation:

[JSExport]
public static void RegisterComponent(string elementID)
{
    if (string.IsNullOrEmpty(elementID))
    {
        throw new ArgumentException("Element ID cannot be null or empty", nameof(elementID));
    }
    
    Console.WriteLine(elementID);
    switch (elementID)
    {
        // ...
    }
}

Copilot uses AI. Check for mistakes.
@LittleLittleCloud LittleLittleCloud enabled auto-merge (squash) November 23, 2025 18:32
@LittleLittleCloud LittleLittleCloud merged commit 7f8b119 into main Nov 23, 2025
6 checks passed
@LittleLittleCloud LittleLittleCloud deleted the u/xiaoyun/wasm branch November 23, 2025 18:35
@github-actions github-actions bot added this to the v0.2.0 milestone Nov 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants