Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
9 changes: 9 additions & 0 deletions extensions/positron-zed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,22 @@
"command": "zed.quartoVisualMode",
"category": "Zed",
"title": "Edit Quarto File in Visual Mode"
},
{
"command": "zed.getConsoleContent",
"category": "Zed",
"title": "Get Console Content"
}
],
"menus": {
"commandPalette": [
{
"category": "Zed",
"command": "zed.quartoVisualMode"
},
{
"category": "Zed",
"command": "zed.getConsoleContent"
}
]
}
Expand Down
35 changes: 35 additions & 0 deletions extensions/positron-zed/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as positron from 'positron';

export async function registerCommands(context: vscode.ExtensionContext) {
context.subscriptions.push(
Expand All @@ -16,5 +17,39 @@ export async function registerCommands(context: vscode.ExtensionContext) {
vscode.commands.executeCommand('positron.reopenWith', editor.document.uri, 'quarto.visualEditor');
}),

// Demo/test the positron.runtime console-history API without needing Posit
// Assistant: reads the foreground session's recent console content and
// opens it in a JSON editor so the result is front and center.
vscode.commands.registerCommand('zed.getConsoleContent', async () => {
const session = await positron.runtime.getForegroundSession();
if (!session) {
vscode.window.showWarningMessage('Zed: No foreground console session to read content from. Start a console first.');
return;
}

// Let the tester exercise the numberOfEntries argument; blank uses the
// API default.
const input = await vscode.window.showInputBox({
title: 'Get Console Content',
prompt: 'Number of most recent console entries to fetch (leave blank for the default)',
validateInput: value => (value === '' || /^\d+$/.test(value) ? undefined : 'Enter a positive whole number, or leave blank for the default.'),
});
if (input === undefined) {
return; // Cancelled.
}
const numberOfEntries = input === '' ? undefined : Number(input);

const sessionId = session.metadata.sessionId;
const entries = await positron.runtime.getConsoleContent(sessionId, numberOfEntries);

// Open the result in an untitled JSON editor: more visible than an
// output channel, and syntax-highlighted for the structured entries.
const document = await vscode.workspace.openTextDocument({
language: 'json',
content: JSON.stringify({ sessionId, numberOfEntries: numberOfEntries ?? null, entryCount: entries.length, entries }, null, 2),
});
await vscode.window.showTextDocument(document, { preview: false });
}),

);
}
41 changes: 41 additions & 0 deletions src/positron-dts/positron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3069,6 +3069,47 @@ declare module 'positron' {
queryTypes: Array<string>):
Thenable<Array<QueryTableSummaryResult>>;

/**
* A single console execution: a command that ran in a runtime session,
* paired with its output and any error.
*/
export interface ConsoleContentEntry {
/** The code that was executed. */
input: string;
/** The textual output produced by the execution. */
output: string;
/** The error produced by the execution, if any. */
error?: {
/** The name of the error. */
name: string;
/** The error message. */
message: string;
/** The error stack trace. */
traceback: string[];
};
/** Time the execution occurred, in milliseconds since the Epoch. */
when: number;
}

/**
* Get the recent console history for a session: the code fragments that
* have already run, each paired with its output and any error. This is
* read-only; it does not execute anything.
*
* Only completed code executions are returned, oldest first. The startup
* banner and entries recorded without input (e.g. output produced outside
* an execution) are omitted, matching what the console shows as a command
* history.
*
* @param sessionId The session ID of the session to read console content
* from.
* @param numberOfEntries The number of most recent entries to return.
* Defaults to 5. Pass a larger value to look further back in the history.
* @returns A Thenable that resolves with the console entries, or an empty
* array when the session has no console history.
*/
export function getConsoleContent(sessionId: string, numberOfEntries?: number): Thenable<ConsoleContentEntry[]>;

/**
* Register a handler for runtime client instances. This handler will be called
* whenever a new client instance is created by a language runtime of the given
Expand Down
12 changes: 10 additions & 2 deletions src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
MainPositronContext,
ExtHostPositronContext,
RuntimeInitialState,
IActiveRuntimeSessionMetadataDto
IActiveRuntimeSessionMetadataDto,
ISerializedConsoleContentEntry
} from '../../common/positron/extHost.positron.protocol.js';
import { extHostNamedCustomer, IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js';
import { IHostedLanguageContribution, ILanguageRuntimeClientCreatedEvent, ILanguageRuntimeInfo, ILanguageRuntimeMessage, ILanguageRuntimeMessageCommClosed, ILanguageRuntimeMessageCommData, ILanguageRuntimeMessageCommOpen, ILanguageRuntimeMessageError, ILanguageRuntimeMessageInput, ILanguageRuntimeMessageOutput, ILanguageRuntimeMessagePrompt, ILanguageRuntimeMessageState, ILanguageRuntimeMessageStream, ILanguageRuntimeMetadata, ILanguageRuntimeSessionState as ILanguageRuntimeSessionState, ILanguageRuntimeService, ILanguageRuntimeStartupFailure, LanguageRuntimeMessageType, RuntimeBusyBehavior, RuntimeCodeExecutionMode, RuntimeCodeFragmentStatus, RuntimeErrorBehavior, RuntimeState, ILanguageRuntimeExit, RuntimeOutputKind, RuntimeExitReason, ILanguageRuntimeMessageWebOutput, PositronOutputLocation, LanguageRuntimeSessionMode, ILanguageRuntimeMessageResult, ILanguageRuntimeMessageClearOutput, ILanguageRuntimeMessageIPyWidget, IRuntimeManager, IRuntimeRootSignature, ILanguageRuntimeMessageUpdateOutput, ILanguageRuntimeResourceUsage, ILanguageRuntimeLaunchInfo } from '../../../services/languageRuntime/common/languageRuntimeService.js';
Expand Down Expand Up @@ -54,6 +55,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js';
import { CodeAttributionSource, IConsoleCodeAttribution } from '../../../services/positronConsole/common/positronConsoleCodeExecution.js';
import { QueryTableSummaryResult, Variable } from '../../../services/languageRuntime/common/positronVariablesComm.js';
import { getSessionVariables, querySessionTables } from '../../../services/positronVariables/common/helpers/sessionVariableQueries.js';
import { IExecutionHistoryService, projectExecutionEntriesToConsoleContent } from '../../../services/positronHistory/common/executionHistoryService.js';
import { isWebviewPreloadMessage, isWebviewReplayMessage } from '../../../services/positronIPyWidgets/common/webviewPreloadUtils.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { LanguageRuntimeDynState } from 'positron';
Expand Down Expand Up @@ -1647,7 +1649,8 @@ export class MainThreadLanguageRuntime
@INotebookService private readonly _notebookService: INotebookService,
@IEditorService private readonly _editorService: IEditorService,
@IOpenerService private readonly _openerService: IOpenerService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@IExecutionHistoryService private readonly _executionHistoryService: IExecutionHistoryService
) {
// TODO@softwarenerd - We needed to find a central place where we could ensure that certain
// Positron services were up and running early in the application lifecycle. For now, this
Expand Down Expand Up @@ -1982,6 +1985,11 @@ export class MainThreadLanguageRuntime
return querySessionTables(this._positronVariablesService, sessionId, accessKeys, queryTypes);
}

async $getConsoleContent(sessionId: string, numberOfEntries?: number): Promise<ISerializedConsoleContentEntry[]> {
return projectExecutionEntriesToConsoleContent(
this._executionHistoryService.getExecutionEntries(sessionId), numberOfEntries);
}

/**
* Emit a performance mark for a given extension. This is used to track the
* timing of startup actions that happen in extensions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce
Thenable<Array<positron.QueryTableSummaryResult>> {
return extHostLanguageRuntime.querySessionTables(sessionId, accessKeys, queryTypes);
},
getConsoleContent(sessionId: string, numberOfEntries?: number): Thenable<positron.runtime.ConsoleContentEntry[]> {
return extHostLanguageRuntime.getConsoleContent(sessionId, numberOfEntries);
},
registerClientHandler(handler: positron.RuntimeClientHandler): vscode.Disposable {
return extHostLanguageRuntime.registerClientHandler(handler);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface MainThreadLanguageRuntimeShape extends IDisposable {
$getSessionWorkingDirectory(sessionId?: string): Promise<string | undefined>;
$getSessionVariables(sessionId: string, accessKeys?: Array<Array<string>>): Promise<Array<Array<Variable>>>;
$querySessionTables(sessionId: string, accessKeys: Array<Array<string>>, queryTypes: Array<string>): Promise<Array<QueryTableSummaryResult>>;
$getConsoleContent(sessionId: string, numberOfEntries?: number): Promise<ISerializedConsoleContentEntry[]>;
$callMethod(sessionId: string, method: string, args: unknown[]): Thenable<unknown>;
$emitPerfMark(extensionId: string, name: string): void;
$emitLanguageRuntimeMessage(sessionId: string, handled: boolean, message: SerializableObjectWithBuffers<ILanguageRuntimeMessage>): void;
Expand Down Expand Up @@ -355,6 +356,13 @@ export type ISerializedValidateAndExecuteCommandResult =
message?: string;
};

export interface ISerializedConsoleContentEntry {
input: string;
output: string;
error?: { name: string; message: string; traceback: string[] };
when: number;
}

export interface MainThreadAiFeaturesShape {
$registerChatAgent(agentData: IChatAgentData): Thenable<void>;
$unregisterChatAgent(id: string): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,10 @@ export class ExtHostLanguageRuntime implements extHostProtocol.ExtHostLanguageRu
return this._proxy.$querySessionTables(sessionId, accessKeys, queryTypes);
}

public getConsoleContent(sessionId: string, numberOfEntries?: number): Promise<extHostProtocol.ISerializedConsoleContentEntry[]> {
return this._proxy.$getConsoleContent(sessionId, numberOfEntries);
}

/**
* Interrupts an active session.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ describe('getEnabledTools', () => {
expect(getEnabledTools(inEditor, tools, true)).toEqual([]);
});

it('keeps a read-only session tool available in Ask mode while excluding executeCode', () => {
const tools = [
tool('executeCode', ['positron-assistant', 'requires-session']),
tool('inspectVariables', ['positron-assistant', 'requires-session']),
];
// Ask mode is the Chat participant (not agent mode); executeCode is
// gated to agent mode, but a read-only session tool stays enabled.
expect(getEnabledTools(request(), tools, true, ParticipantID.Chat)).toEqual(['inspectVariables']);
});

it('disables all tools for the terminal participant', () => {
const tools = [tool('foo')];
expect(getEnabledTools(request(), tools, true, ParticipantID.Terminal)).toEqual([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,50 @@ export interface IExecutionHistoryError {
traceback: string[];
}

/**
* A single console execution projected to the fields relevant to a model or an
* extension: the code that ran, its output, and any error.
*/
export interface IConsoleContentEntry {
/** The code that was executed. */
input: string;
/** The textual output produced by the execution. */
output: string;
/** The error produced by the execution, if any. */
error?: IExecutionHistoryError;
/** Time the execution occurred, in milliseconds since the Epoch. */
when: number;
}

/** Default number of recent console entries returned when no count is requested. */
export const DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT = 5;

/**
* Projects raw execution history entries down to the console content relevant
* to a reader: only completed code executions (skipping the startup banner and
* entries recorded without input, e.g. output produced outside an execution),
* each mapped to its input, textual output, error, and timestamp, and limited
* to the most recent `numberOfEntries` (oldest first, so a reader sees them in
* chronological order).
*
* @param entries The raw execution history entries, in stored (oldest-first) order.
* @param numberOfEntries The number of most recent entries to return. Defaults to
* {@link DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT}; non-positive values fall back to it.
*/
export function projectExecutionEntriesToConsoleContent(entries: IExecutionHistoryEntry<unknown>[], numberOfEntries?: number): IConsoleContentEntry[] {
const projected = entries
.filter(entry => entry.outputType === ExecutionEntryType.Execution && entry.input)
.map(entry => ({
input: entry.input,
output: typeof entry.output === 'string' ? entry.output : String(entry.output ?? ''),
error: entry.error,
when: entry.when,
}));

const count = numberOfEntries && numberOfEntries > 0 ? numberOfEntries : DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT;
return projected.slice(-count);
}

/**
* Represents an input code fragment sent to a language runtime.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js
import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { ILanguageRuntimeSession, IRuntimeSessionService, RuntimeStartMode, ILanguageRuntimeSessionStateEvent, ILanguageRuntimeGlobalEvent, IRuntimeSessionMetadata, IRuntimeSessionWillStartEvent, INotebookSessionUriChangedEvent, INotebookLanguageRuntimeSession, IRuntimeSessionDisplayInfo } from '../../../../services/runtimeSession/common/runtimeSessionService.js';
import { IExecutionHistoryService, ExecutionEntryType } from '../../common/executionHistoryService.js';
import { IExecutionHistoryService, ExecutionEntryType, IExecutionHistoryEntry, projectExecutionEntriesToConsoleContent, DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT } from '../../common/executionHistoryService.js';
import { IRuntimeAutoStartEvent, IRuntimeStartupService, ISessionRestoreFailedEvent, SerializedSessionMetadata } from '../../../../services/runtimeStartup/common/runtimeStartupService.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { ExecutionHistoryService } from '../../common/executionHistory.js';
Expand Down Expand Up @@ -1061,3 +1061,61 @@ describe('ExecutionHistoryService', () => {
expect(storeSpy).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`positron\\..*\\.${sessionId}`)), null, expect.any(Number), expect.any(Number));
});
});

describe('projectExecutionEntriesToConsoleContent', () => {
/** Build an execution history entry, defaulting to a completed code execution. */
function entry(overrides: Partial<IExecutionHistoryEntry<unknown>>): IExecutionHistoryEntry<unknown> {
return {
id: 'id',
when: 0,
prompt: '',
input: '',
outputType: ExecutionEntryType.Execution,
output: '',
durationMs: 0,
...overrides,
};
}

it('projects completed executions to input/output/error/when, oldest first', () => {
const entries = [
entry({ input: 'a <- 1', output: '', when: 1 }),
entry({ input: 'stop("boom")', output: '', error: { name: 'error', message: 'boom', traceback: [] }, when: 2 }),
entry({ input: 'print(2)', output: '[1] 2\n', when: 3 }),
];

expect(projectExecutionEntriesToConsoleContent(entries)).toEqual([
{ input: 'a <- 1', output: '', when: 1 },
{ input: 'stop("boom")', output: '', error: { name: 'error', message: 'boom', traceback: [] }, when: 2 },
{ input: 'print(2)', output: '[1] 2\n', when: 3 },
]);
});

it('excludes the startup banner, entries without input, and coerces non-string output', () => {
const entries = [
entry({ outputType: ExecutionEntryType.Startup, input: '', output: 'banner', when: 1 }),
entry({ input: '', output: 'orphan output', when: 2 }),
entry({ input: 'real()', output: 42, when: 3 }),
];

expect(projectExecutionEntriesToConsoleContent(entries)).toEqual([
{ input: 'real()', output: '42', when: 3 },
]);
});

it('limits to the most recent DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT when no count is given', () => {
const entries = Array.from({ length: 8 }, (_, i) => entry({ input: `cmd${i}`, when: i }));

const result = projectExecutionEntriesToConsoleContent(entries);

expect(result.map(e => e.input)).toEqual(['cmd3', 'cmd4', 'cmd5', 'cmd6', 'cmd7']);
expect(result).toHaveLength(DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT);
});

it('limits to the most recent numberOfEntries when given, and falls back to the default for non-positive values', () => {
const entries = Array.from({ length: 8 }, (_, i) => entry({ input: `cmd${i}`, when: i }));

expect(projectExecutionEntriesToConsoleContent(entries, 2).map(e => e.input)).toEqual(['cmd6', 'cmd7']);
expect(projectExecutionEntriesToConsoleContent(entries, 0)).toHaveLength(DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT);
});
});
Loading