Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a4d365d
Implement async channel
lionel- Jun 12, 2025
1ae57db
Draft unmanaged comms
lionel- Jun 12, 2025
3d386bc
Clean up comm channel when disposed
lionel- Jun 12, 2025
6a962cd
Make raw comm bidirectional
lionel- Jun 12, 2025
cb4764a
Clean up client when removed
lionel- Jun 12, 2025
21726b8
Make `RawComm` a disposable
lionel- Jun 13, 2025
201d67c
Add `reject()` method
lionel- Jun 13, 2025
e8a41e2
Reorganise files and exports
lionel- Jun 13, 2025
a0e2088
Split channel into tx and rx parts
lionel- Jun 13, 2025
8b89219
Return boolean from `send()` to indicate whether channel was still open
lionel- Jun 13, 2025
cd14914
Take handlers on comm creation instead of returning channel
lionel- Jun 13, 2025
73036e0
Streamline comm closing
lionel- Jun 13, 2025
dcefb60
Use `switch`
lionel- Jun 13, 2025
67bc9e4
Use standard `target_name` parameter name
lionel- Jun 13, 2025
698028e
Revert to async-iterable approach after all
lionel- Jun 16, 2025
bf7f359
Make comm ID public
lionel- Aug 10, 2025
991cd9e
Use camelCase
lionel- Aug 10, 2025
86e4bad
Reformat utils
lionel- Aug 13, 2025
0981470
Implement Ark DAP as raw comm
lionel- Jun 16, 2025
9730abf
Don't list unmanaged comms as clients
lionel- Aug 19, 2025
d90a7e7
Remove `positron.dap` from client types
lionel- Aug 19, 2025
883fd68
Log `start_debug` errors
lionel- Aug 19, 2025
de2a82a
Rename `DapClient.ts` to `DapComm.ts`
lionel- Aug 19, 2025
9621560
Streamline and document `DapComm` in supervisor API
lionel- Aug 19, 2025
32d9a52
Make `handleMessage()` async
lionel- Aug 19, 2025
1419051
Review comm documentation
lionel- Aug 19, 2025
7390a8b
Rename `Channel` to `ReceiverChannel`
lionel- Aug 19, 2025
74148bc
Add `supervisorApi()` accessor
lionel- Aug 19, 2025
530b2f1
Move check inside
lionel- Aug 19, 2025
d55fc83
Await request result
lionel- Aug 19, 2025
ca2dc91
Don't close if already closed
lionel- Aug 20, 2025
8743714
Add tests for raw comms
lionel- Aug 20, 2025
023c2aa
Note that requests from backend to frontend are currently not possible
lionel- Aug 20, 2025
ff71b2e
Throw errors from `notify()` and `request()`
lionel- Aug 20, 2025
5cb0d4e
Rename `Comm` to `Client`
lionel- Aug 20, 2025
ff60387
Add comments from pair review with Davis
lionel- Aug 21, 2025
61a2dad
Don't dispose of comm in Kallichore
lionel- Aug 21, 2025
2d47608
Store `RawCommImpl` instead of `RawComm`
lionel- Aug 21, 2025
b0b29f3
Don't expose whole DAP comm class through supervisor API
lionel- Aug 21, 2025
2fbbf97
Make comm errors a tagged union
lionel- Aug 21, 2025
f8be357
Include `id` field in nested JSON-RPC requests
lionel- Aug 21, 2025
2da8743
Use JSON-RPC `id` field if present
lionel- Aug 21, 2025
dca1842
Bump Ark
lionel- Aug 21, 2025
23f3d80
Rename `RawComm` to `Comm`
lionel- Aug 21, 2025
3772666
No longer `host` but `ip_address`
lionel- Aug 21, 2025
5d3ed25
Remove `createComm()` from public interface of `DapComm`
lionel- Aug 28, 2025
a8f46dc
Address code review
lionel- Sep 20, 2025
a6abaab
Bump Ark to 0.1.210
lionel- Sep 20, 2025
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
242 changes: 226 additions & 16 deletions extensions/positron-python/src/client/positron-supervisor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,6 @@ export interface JupyterLanguageRuntimeSession extends positron.LanguageRuntimeS
*/
startPositronLsp(clientId: string, ipAddress: string): Promise<number>;

/**
* Convenience method for starting the Positron DAP server, if the
* language runtime supports it.
*
* @param clientId The ID of the client comm, created with
* `createPositronDapClientId()`.
* @param debugType Passed as `vscode.DebugConfiguration.type`.
* @param debugName Passed as `vscode.DebugConfiguration.name`.
*/
startPositronDap(clientId: string, debugType: string, debugName: string): Promise<void>;

/**
* Convenience method for creating a client id to pass to
* `startPositronLsp()`. The caller can later remove the client using this
Expand All @@ -110,11 +99,64 @@ export interface JupyterLanguageRuntimeSession extends positron.LanguageRuntimeS
createPositronLspClientId(): string;

/**
* Convenience method for creating a client id to pass to
* `startPositronDap()`. The caller can later remove the client using this
* id as well.
* Start a comm for communication between frontend and backend.
*
* Unlike Positron clients, this kind of comm is private to the calling
* extension and its kernel. They open a direct line of communication that
* lives entirely on the extension host.
*
* The messages sent over these comms are expected to conform to JSON-RPC:
* - The message type is encoded in the `method` field.
* - Parameters are optional and encoded as object (named list of
* parameters) in the `params` field.
* - If a response is expected, add an `id` field to indicate that this is a
* request. This ID field is redundant with the one used in the Jupyter
* layer but allows applications to make a distinction between notifications
* and requests.
*
* Responses to requests follow this format:
* - `result` or `error` field.
* - `id` field corresponding to the request's `id`.
*
* @param target_name Comm type, also used to generate comm identifier.
* @param params Optionally, additional parameters included in `comm_open`.
*/
createComm(target_name: string, params?: Record<string, unknown>): Promise<Comm>;

/**
* Create a server comm.
*
* Server comms are a special type of comms (see `createComm()`) that
* wrap a TCP server (e.g. an LSP or DAP server). The backend is expected to
* handle `comm_open` messages for `targetName` comms in the following way:
*
* - The `comm_open` messages includes an `ip_address` field. The server
* must be started on this addess. The server, and not the client, picks
* the port to prevent race conditions where a port becomes used between
* the time it was picked by the frontend and handled by the backend.
*
* - Once the server is started at `ip_address` on a port, the backend sends
* back a notification message of type (method) `server_started` that
* includes a field `port`.
*
* @param targetName The name of the comm target
* @param ip_address The IP address to which the server should bind to.
* @returns A promise that resolves to a tuple of [Comm, port number]
* once the server has been started on the backend side.
*/
createServerComm(targetName: string, ip_address: string): Promise<[Comm, number]>;

/**
* Constructs a new DapComm instance.
* Must be disposed. See `DapComm` documentation.
*
* @param session The Jupyter language runtime session.
* @param targetName The name of the comm target.
* @param debugType The type of debugger, as required by `vscode.DebugConfiguration.type`.
* @param debugName The name of the debugger, as required by `vscode.DebugConfiguration.name`.
* @returns A new `DapComm` instance.
*/
createPositronDapClientId(): string;
createDapComm(targetName: string, debugType: string, debugName: string): Promise<DapComm>;

/**
* Method for emitting a message to the language server's Jupyter output
Expand All @@ -129,7 +171,8 @@ export interface JupyterLanguageRuntimeSession extends positron.LanguageRuntimeS
* A Jupyter kernel is guaranteed to have a `showOutput()`
* method, so we declare it non-optional.
*
* @param channel The channel to show the output of.
* @param channel The name of the output channel to show.
* If not provided, the default channel is shown.
*/
showOutput(channel?: positron.LanguageRuntimeSessionChannel): void;

Expand Down Expand Up @@ -210,3 +253,170 @@ export interface JupyterKernelExtra {
init: (args: Array<string>, delay: number) => void;
};
}

/**
* Comm between an extension and its kernel.
*
* This type of comm is not mapped to a Positron client. It lives entirely in
* the extension space and allows a direct line of communication between an
* extension and its kernel.
*
* It's a disposable. Dispose of it once it's closed or you're no longer using
* it. If the comm has not already been closed by the kernel, a client-initiated
* `comm_close` message is emitted to clean the comm on the backend side.
*/
export interface Comm {
/** The comm ID. */
id: string;

/**
* Async-iterable for messages sent from backend.
*
* - This receiver channel _must_ be awaited and handled to exhaustion.
* - When exhausted, you _must_ dispose of the comm.
*
* Yields `CommBackendMessage` messages which are a tagged union of
* notifications and requests. If a request, the `handle` method _must_ be
* called (see `CommBackendMessage` documentation).
*/
receiver: ReceiverChannel<CommBackendMessage>;

/**
* Send a notification to the backend comm.
* Throws `CommClosedError` if comm was closed.
*/
notify: (method: string, params?: Record<string, unknown>) => void;

/**
* Make a request to the backend comm.
*
* Resolves when backend responds with the result.
* Throws:
* - `CommClosedError` if comm was closed
* - `CommRpcError` for RPC errors.
*/
request: (method: string, params?: Record<string, unknown>) => Promise<any>;

/** Clear resources and sends `comm_close` to backend comm (unless the channel
* was closed by the backend already). */
dispose: () => Promise<void>;
}

/**
* Async-iterable receiver channel for comm messages from the backend.
* The messages are buffered and must be received as long as the channel is open.
* Dispose to close.
*/
export interface ReceiverChannel<T> extends AsyncIterable<T>, vscode.Disposable {
next(): Promise<IteratorResult<T>>;
}

/**
* Base class for communication errors.
*/
export interface CommError extends Error {
readonly name: 'CommError' | 'CommClosedError' | 'CommRpcError';
readonly method?: string;
}

/**
* Error thrown when attempting to communicate through a closed channel.
*/
export interface CommClosedError extends CommError {
readonly name: 'CommClosedError';
}

/**
* Error thrown for RPC-specific errors with error codes.
*/
export interface CommRpcError extends CommError {
readonly name: 'CommRpcError';
readonly code: number;
}

/**
* Message from the backend.
*
* If a request, the `handle` method _must_ be called.
* Throw an error from `handle` to reject the request (e.g. if `method` is unknown).
*
* Note: Requests are currently not possible, see
* <https://github.com/posit-dev/positron/issues/2061>
*/
export type CommBackendMessage =
| {
kind: 'request';
method: string;
params?: Record<string, unknown>;
handle: (handler: () => any) => void;
}
| {
kind: 'notification';
method: string;
params?: Record<string, unknown>;
};

/**
* A Debug Adapter Protocol (DAP) comm.
*
* This wraps a `Comm` that:
*
* - Implements the server protocol (see `createComm()` and
* `JupyterLanguageRuntimeSession::createServerComm()`).
*
* - Optionally handles a standard set of DAP comm messages.
*
* Must be disposed when no longer in use or if `comm.receiver` is exhausted.
* Disposing the `DapComm` automatically disposes of the nested `Comm`.
*/
export interface DapComm {
/** The `targetName` passed to the constructor. */
readonly targetName: string;

/** The `debugType` passed to the constructor. */
readonly debugType: string;

/** The `debugName` passed to the constructor. */
readonly debugName: string;

/**
* The comm for the DAP.
* Use it to receive messages or make notifications and requests.
* Defined after `createServerComm()` has been called.
*/
readonly comm?: Comm;

/**
* The port on which the DAP server is listening.
* Defined after `createServerComm()` has been called.
*/
readonly serverPort?: number;

/**
* Handle a message received via `this.comm.receiver`.
*
* This is optional. If called, these message types are handled:
*
* - `start_debug`: A debugging session is started from the frontend side,
* connecting to `this.serverPort`.
*
* - `execute`: A command is visibly executed in the console. Can be used to
* handle DAP requests like "step" via the console, delegating to the
* interpreter's own debugging infrastructure.
*
* - `restart`: The console session is restarted. Can be used to handle a
* restart DAP request on the backend side.
*
* Returns whether the message was handled. Note that if the message was not
* handled, you _must_ check whether the message is a request, and either
* handle or reject it in that case.
*/
handleMessage(msg: any): Promise<boolean>;

/**
* Dispose of the underlying comm.
* Must be called if the DAP comm is no longer in use.
* Closes the comm if not done already.
*/
dispose(): void;
}
2 changes: 1 addition & 1 deletion extensions/positron-r/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,7 @@
},
"positron": {
"binaryDependencies": {
"ark": "0.1.209"
"ark": "0.1.210"
},
"minimumRVersion": "4.2.0",
"minimumRenvVersion": "1.0.9"
Expand Down
35 changes: 35 additions & 0 deletions extensions/positron-r/src/ark-comm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { JupyterLanguageRuntimeSession, Comm } from './positron-supervisor';
import { LOGGER } from './extension';

/**
* Communication channel with the bakend.
* Currently only used for testing.
*/
export class ArkComm implements vscode.Disposable {
readonly targetName: string = 'ark';

public get comm(): Comm | undefined {
return this._comm;
}

private _comm?: Comm;

constructor(
private session: JupyterLanguageRuntimeSession,
) { }

async createComm(): Promise<void> {
this._comm = await this.session.createComm(this.targetName);
LOGGER.info(`Created Ark comm with ID: ${this._comm.id}`);
}

async dispose(): Promise<void> {
await this._comm?.dispose();
}
}
14 changes: 14 additions & 0 deletions extensions/positron-r/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { RRuntimeManager } from './runtime-manager';
import { registerUriHandler } from './uri-handler';
import { registerRLanguageModelTools } from './llm-tools.js';
import { registerFileAssociations } from './file-associations.js';
import { PositronSupervisorApi } from './positron-supervisor';

export const LOGGER = vscode.window.createOutputChannel('R Language Pack', { log: true });

Expand Down Expand Up @@ -53,3 +54,16 @@ export function activate(context: vscode.ExtensionContext) {
}
});
}

export async function supervisorApi(): Promise<PositronSupervisorApi> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From review with Davis:

We might be able to make activate() async, then await the supervisor API, then set a global SUPERVISOR_API. Then supervisorApi() no longer needs to be async, which would make it much more ergonomic:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would allow us to export error classes and use instanceof to match them, without contagious asyncness.

const ext = vscode.extensions.getExtension('positron.positron-supervisor');
if (!ext) {
throw new Error('Positron Supervisor extension not found');
}

if (!ext.isActive) {
await ext.activate();
}

return ext?.exports as PositronSupervisorApi;
}
Loading
Loading