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
2 changes: 1 addition & 1 deletion Verso.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
Expand Down
19 changes: 19 additions & 0 deletions docs/architecture/execution-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ All three methods call `EnsureParametersInjected()` first, which pushes notebook
- `Func<Guid, string?>` -- language ID resolution for a cell
- `Func<Guid, int>` -- execution count lookup
- `Func<string, IMagicCommand?>` -- magic command resolution delegate
- Optional `Action<Guid>` -- notified when a running cell appends output
- Optional input requester delegate -- asks the current host for interactive input

The pipeline has no direct reference to `Scaffold`. All access goes through these delegates and interfaces.

Expand Down Expand Up @@ -96,11 +98,14 @@ See the [Magic Commands](#magic-commands) section below for details.

An `ExecutionContext` is constructed with the remaining code (after magic commands are stripped), the variable store, theme context, and all other shared services. This context implements `IExecutionContext`, which extends `IVersoContext`.

If the host supplied an input requester, the execution context exposes it through `RequestInputAsync(prompt, isPassword, ct)`. Kernels can use this to pause execution until the front-end returns a value. Hosts that do not provide an input requester keep the default unsupported behavior.

### 6. Execute

The kernel's `ExecuteAsync(code, context)` is called. The kernel can:

- **Stream outputs** during execution via `context.WriteOutputAsync(output)`, which calls the `AppendOutput` delegate
- **Request interactive input** during execution via `context.RequestInputAsync(...)`
- **Return outputs** from the method, which are appended after execution (skipping any already streamed)

### 7. Post-Processing
Expand Down Expand Up @@ -197,6 +202,20 @@ Outputs are collected in `cell.Outputs` (a mutable `List<CellOutput>`). The pipe

Error outputs set `IsError = true` and optionally include `ErrorName` (exception type) and `ErrorStackTrace`.

### Live Output Notifications

When a kernel calls `WriteOutputAsync`, the pipeline appends the output immediately and invokes the optional output-updated callback. `Scaffold` exposes this as `OnCellOutputUpdated`, and interactive hosts can forward it to their UI before the cell finishes executing. VS Code uses this path to send `output/update` and refresh the cell output cache while the `execution/run` request is still pending.

This mechanism streams outputs that are explicitly written through the execution context. Outputs that are only returned from `ILanguageKernel.ExecuteAsync` are still appended after the kernel method completes.

## Interactive Input

`IExecutionContext.RequestInputAsync(prompt, isPassword, ct)` asks the current host to collect a single line of input. It returns the entered value or `null` when the user cancels. The default interface implementation throws `NotSupportedException`, so kernels should treat this as an optional host capability.

The PowerShell kernel uses this capability through its internal host adapter around PowerShell's `PSHost` / `PSHostUserInterface` APIs. It maps host prompts such as `Read-Host`, credential prompts, and choice prompts onto `RequestInputAsync`. In VS Code, the request is delivered as an `input/request` notification and answered with `input/response`.

PowerShell host output (`Write-Host`, warnings, errors, verbose/debug output, and similar `PSHostUserInterface` writes) is converted to `text/plain` `CellOutput` values and written through `WriteOutputAsync`. ANSI escape sequences are currently stripped before display so host output stays readable in cell output. Future work may parse supported ANSI SGR sequences into safe rich output instead of discarding them.

## Execution Result

`ExecutionResult` is an immutable record returned by the pipeline:
Expand Down
35 changes: 33 additions & 2 deletions docs/architecture/front-ends.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Engine events are forwarded to the UI through `Action?` events on the service, w

### Project References

Verso.Blazor references the engine (`Verso`), all kernel extension packages (`Verso.FSharp`, `Verso.JavaScript`, `Verso.Python`, `Verso.PowerShell`, `Verso.Ado`, `Verso.Http`), and the shared UI library (`Verso.Blazor.Shared`).
Verso.Blazor references the engine (`Verso`), all kernel extension packages (`Verso.FSharp`, `Verso.JavaScript`, `Verso.Python`, `Verso.PowerShell`, `Verso.Ado`, `Verso.Http`), and the shared UI library (`Verso.Blazor.Shared`). The PowerShell host adapter is internal to `Verso.PowerShell`.

## VS Code Extension

Expand Down Expand Up @@ -154,6 +154,8 @@ The WASM project runs in the webview sandbox. It references only `Verso.Abstract

The WASM app maintains a local state cache populated from the `notebook/opened` notification. Source updates are debounced (250ms) before forwarding to the host to avoid flooding the JSON-RPC channel during fast typing.

Some requests use a detached bridge path instead of awaiting the full JSON-RPC round-trip inside the webview call stack. This allows the webview to keep processing host notifications while a long-running request such as `execution/run` is still pending.

`WebviewNavigationManager` stubs `NavigationManager` with `app:///` as the base URI because the `vscode-webview://` scheme is not parseable by `System.Uri`.

### Message Flow
Expand Down Expand Up @@ -182,13 +184,40 @@ Cell.razor calls NotebookService.ExecuteCellAsync(cellId)

Notifications (execution state changes, variable updates) flow in the reverse direction without a request ID.

### Live Output and Interactive Input

Kernels can append outputs while a cell is still running. In VS Code, this uses the `output/update` host notification:

```
Kernel calls context.WriteOutputAsync(output)
-> ExecutionPipeline appends the output and notifies Scaffold
-> HostSession sends output/update { notebookId, cellId }
-> BlazorBridge forwards the notification to the webview
-> RemoteNotebookService refreshes the local cell output cache
-> Notebook components re-render before execution/run completes
```

Kernels can also request a single input value through `IExecutionContext.RequestInputAsync`. VS Code handles this with `input/request` and `input/response`:

```
Kernel calls context.RequestInputAsync(prompt, isPassword, ct)
-> NotebookSession creates a pending input request
-> HostSession sends input/request { notebookId, requestId, cellId, prompt, isPassword }
-> BlazorBridge shows a VS Code input box
-> BlazorBridge sends input/response { notebookId, requestId, value, cancelled }
-> NotebookSession resolves the pending request
-> Kernel execution resumes
```

`input/response` is handled by the host read loop outside the normal sequential request queue. This is required because the queued `execution/run` request is still active while the kernel is waiting for the user's answer.

## Verso.Host

`Verso.Host` is a console application that serves as the engine host for VS Code. It communicates via line-delimited JSON-RPC on stdin/stdout.

### Startup

`Program.cs` sets console encoding to UTF-8, emits a `host/ready` notification, then enters a read loop on stdin. A shared `stdoutLock` ensures atomic response writes. Incoming messages are queued through a `Channel<T>` and dispatched sequentially by `HostSession`.
`Program.cs` sets console encoding to UTF-8, emits a `host/ready` notification, then enters a read loop on stdin. A shared `stdoutLock` ensures atomic response writes. Incoming messages are queued through a `Channel<T>` and dispatched sequentially by `HostSession`. A small set of re-entrant responses, such as `extension/consentResponse` and `input/response`, are handled directly in the read loop so they can unblock an already-running request.

### HostSession and NotebookSession

Expand All @@ -213,6 +242,8 @@ Notifications (execution state changes, variable updates) flow in the reverse di

Method names are centralized in `Protocol.MethodNames`. The TypeScript `protocol.ts` in the VS Code extension mirrors the same method names.

`NotebookSession` also owns session-scoped callbacks that are not exposed as standalone handler classes. It subscribes to `Scaffold.OnCellOutputUpdated` and sends `output/update`, and it implements the pending request table for `input/request` / `input/response`.

## CLI (Verso.Cli)

The CLI is a .NET global tool with four commands. It references all kernel packages and the Blazor Server project.
Expand Down
8 changes: 6 additions & 2 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The architecture separates into three layers. The engine knows nothing about the
| +------------------------------------------------------+ |
| | First-Party Extension Packages | |
| | Verso.FSharp - Verso.JavaScript - Verso.PowerShell | |
| | Verso.Python - Verso.Ado (SQL) - Verso.Http | |
| | Verso.Python - Verso.Ado - Verso.Http | |
| +------------------------------------------------------+ |
+-----------------------------------------------------------+
|
Expand Down Expand Up @@ -89,6 +89,8 @@ The engine is a headless .NET library with no UI dependencies. Its central class

The engine is designed for embedding. Any .NET application can reference the Verso NuGet package, create a `Scaffold`, load extensions, and execute notebook cells programmatically. The front-ends are consumers of this API, not part of it.

Execution contexts can still request host services through public abstractions. Kernels use `WriteOutputAsync` for live output and `RequestInputAsync` for a single interactive input value. Hosts that support those operations wire them to their UI; hosts that do not support them can keep the default `NotSupportedException` behavior.

See the [engine](engine.md) and [execution-pipeline](execution-pipeline.md) documents for detailed coverage.

### Front-Ends
Expand All @@ -99,6 +101,8 @@ Three front-ends consume the engine:

**VS Code** runs Blazor WebAssembly in a webview. The WASM app has no reference to the engine. Instead, a `RemoteNotebookService` forwards all operations through a JavaScript bridge to the VS Code extension, which relays them over JSON-RPC (stdin/stdout) to a `Verso.Host` process. The host process holds the `Scaffold` and runs the engine. This separation means the UI runs in the browser's sandbox while the engine has full .NET runtime access.

During long-running execution, VS Code also receives host-pushed notifications for live output and interactive input. `output/update` refreshes cell outputs before the final `execution/run` response returns, and `input/request` asks the extension to collect a value from the user and answer with `input/response`.

**The CLI** (`verso run`, `verso convert`) drives the engine headlessly with no UI. A `HeadlessRunner` creates a `Scaffold`, loads extensions, resolves parameters, and executes cells sequentially. Output is rendered to the terminal or serialized to JSON. This is the entry point for CI/CD pipelines.

All three visual front-ends (Blazor Server, VS Code, and `verso serve`) share the same Razor components from the `Verso.Blazor.Shared` class library, so the notebook experience is identical regardless of hosting environment.
Expand Down Expand Up @@ -135,7 +139,7 @@ Verso (engine: Scaffold, ExtensionHost, Pipeline)
^
|--- Verso.FSharp (F# kernel extension)
|--- Verso.JavaScript (JS/TS kernel extension)
|--- Verso.PowerShell (PowerShell kernel extension)
|--- Verso.PowerShell (PowerShell kernel extension with internal PSHost adapter)
|--- Verso.Python (Python kernel extension)
|--- Verso.Ado (SQL kernel extension)
|--- Verso.Http (HTTP kernel extension)
Expand Down
22 changes: 21 additions & 1 deletion docs/extensions/context-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Extends `IVersoContext` with execution-specific state. Passed to `ILanguageKerne
| `CellId` | `Guid` | Unique identifier of the cell being executed. |
| `ExecutionCount` | `int` | Monotonically increasing execution counter for the current cell. |
| `DisplayAsync(CellOutput)` | `Task` | Sends a display output that can be updated in place during execution. |
| `RequestInputAsync(string, bool, CancellationToken)` | `Task<string?>` | Requests a single input value from the current host. The second argument controls password masking. Returns `null` when the user cancels. Default implementation throws `NotSupportedException` on non-interactive hosts. |

### When Available

Expand All @@ -72,6 +73,12 @@ Only within `ILanguageKernel.ExecuteAsync`. Not available during completions, di
- `DisplayAsync` sends a live-updating display that can be replaced. Use it for progress indicators, streaming output, or interactive displays.
- `UpdateOutputAsync` (from `IVersoContext`) replaces the content of a specific output block by ID. Use it for interactive panels that refresh in place (e.g., paginated tables, `ICellInteractionHandler` responses).

### Interactive Input

`RequestInputAsync` is optional host functionality. Use it when a kernel genuinely needs a value from the user during execution, and handle `NotSupportedException` if the same code can run in headless or non-interactive environments.

In VS Code, input requests are shown with a native input box. In headless hosts, the default implementation is unsupported unless that host wires in its own input provider.

### Usage Example

```csharp
Expand All @@ -87,9 +94,22 @@ public async Task<IReadOnlyList<CellOutput>> ExecuteAsync(string code, IExecutio
}
```

```csharp
public async Task<IReadOnlyList<CellOutput>> ExecuteAsync(string code, IExecutionContext context)
{
var name = await context.RequestInputAsync("Name:", cancellationToken: context.CancellationToken);
if (name is null)
{
return new[] { new CellOutput("text/plain", "Input cancelled.") };
}

return new[] { new CellOutput("text/plain", $"Hello, {name}") };
}
```

### Test Stub

`StubExecutionContext` from `Verso.Testing.Stubs` tracks both `WrittenOutputs` and `DisplayedOutputs` as `List<CellOutput>` for assertion.
`StubExecutionContext` from `Verso.Testing.Stubs` tracks both `WrittenOutputs` and `DisplayedOutputs` as `List<CellOutput>` for assertion. Tests can set `InputHandler` to provide values for `RequestInputAsync`.

---

Expand Down
15 changes: 15 additions & 0 deletions src/Verso.Abstractions/Contexts/IExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,19 @@ public interface IExecutionContext : IVersoContext
/// <param name="output">The cell output to display.</param>
/// <returns>A task that completes when the output has been displayed.</returns>
Task DisplayAsync(CellOutput output);

/// <summary>
/// Requests a single line of user input from the current front-end.
/// </summary>
/// <param name="prompt">Prompt text shown to the user.</param>
/// <param name="isPassword">Whether the input should be masked.</param>
/// <param name="cancellationToken">Cancellation token for the pending input request.</param>
/// <returns>The entered value, or <c>null</c> when the user cancels the prompt.</returns>
Task<string?> RequestInputAsync(
string prompt,
bool isPassword = false,
CancellationToken cancellationToken = default)
{
throw new NotSupportedException("Interactive input is not supported by this host.");
}
}
38 changes: 36 additions & 2 deletions src/Verso.Blazor.Wasm/Services/RemoteNotebookService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
private string? _title;
private string? _defaultKernelId;
private List<KernelLanguageInfo> _registeredLanguages = new();
private DateTimeOffset? _created;

Check warning on line 24 in src/Verso.Blazor.Wasm/Services/RemoteNotebookService.cs

View workflow job for this annotation

GitHub Actions / .NET (ubuntu-latest)

Field 'RemoteNotebookService._created' is never assigned to, and will always have its default value

Check warning on line 24 in src/Verso.Blazor.Wasm/Services/RemoteNotebookService.cs

View workflow job for this annotation

GitHub Actions / .NET (windows-latest)

Field 'RemoteNotebookService._created' is never assigned to, and will always have its default value

Check warning on line 24 in src/Verso.Blazor.Wasm/Services/RemoteNotebookService.cs

View workflow job for this annotation

GitHub Actions / .NET (macos-latest)

Field 'RemoteNotebookService._created' is never assigned to, and will always have its default value

Check warning on line 24 in src/Verso.Blazor.Wasm/Services/RemoteNotebookService.cs

View workflow job for this annotation

GitHub Actions / NuGet Validation

Field 'RemoteNotebookService._created' is never assigned to, and will always have its default value
private DateTimeOffset? _modified;

Check warning on line 25 in src/Verso.Blazor.Wasm/Services/RemoteNotebookService.cs

View workflow job for this annotation

GitHub Actions / .NET (ubuntu-latest)

Field 'RemoteNotebookService._modified' is never assigned to, and will always have its default value

Check warning on line 25 in src/Verso.Blazor.Wasm/Services/RemoteNotebookService.cs

View workflow job for this annotation

GitHub Actions / .NET (windows-latest)

Field 'RemoteNotebookService._modified' is never assigned to, and will always have its default value

Check warning on line 25 in src/Verso.Blazor.Wasm/Services/RemoteNotebookService.cs

View workflow job for this annotation

GitHub Actions / .NET (macos-latest)

Field 'RemoteNotebookService._modified' is never assigned to, and will always have its default value

Check warning on line 25 in src/Verso.Blazor.Wasm/Services/RemoteNotebookService.cs

View workflow job for this annotation

GitHub Actions / NuGet Validation

Field 'RemoteNotebookService._modified' is never assigned to, and will always have its default value
private string _formatVersion = "";
private List<CellModel> _cells = new();
private List<CellTypeInfo> _cellTypes = new();
Expand Down Expand Up @@ -770,7 +770,7 @@
_ = HandleExtensionChangedAsync();
break;
case "output/update":
HandleOutputUpdate();
HandleOutputUpdate(paramsJson);
break;
case "kernel/restarting":
HandleKernelRestarting(paramsJson);
Expand Down Expand Up @@ -838,8 +838,36 @@
OnNotebookChanged?.Invoke();
}

private void HandleOutputUpdate()
private void HandleOutputUpdate(string? paramsJson)
{
if (!string.IsNullOrWhiteSpace(paramsJson))
{
try
{
var notif = JsonSerializer.Deserialize<OutputUpdateNotification>(
paramsJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

if (notif is not null && Guid.TryParse(notif.CellId, out var cellId))
{
var cell = _cells.FirstOrDefault(c => c.Id == cellId);
if (cell is not null)
{
cell.Outputs.Clear();
foreach (var output in notif.Outputs ?? Enumerable.Empty<CellOutputDto>())
cell.Outputs.Add(MapOutputFromDto(output));

OnOutputUpdated?.Invoke();
return;
}
}
}
catch (JsonException)
{
// Fall back to a full refresh below.
}
}

_ = RefreshCellListAsync().ContinueWith(_ => OnOutputUpdated?.Invoke());
}

Expand Down Expand Up @@ -1220,6 +1248,12 @@
public string State { get; set; } = "";
}

private sealed class OutputUpdateNotification
{
public string CellId { get; set; } = "";
public List<CellOutputDto>? Outputs { get; set; }
}

private sealed class SaveResponse
{
public string? Content { get; set; }
Expand Down
Loading
Loading