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
9 changes: 9 additions & 0 deletions extensions/positron-zed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,22 @@
"args": [],
"returns": "void"
}
},
{
"command": "zed.getConsoleHistory",
"category": "Zed",
"title": "Get Console History"
}
],
"menus": {
"commandPalette": [
{
"category": "Zed",
"command": "zed.quartoVisualMode"
},
{
"category": "Zed",
"command": "zed.getConsoleHistory"
}
]
}
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 history and
// opens it in a JSON editor so the result is front and center.
vscode.commands.registerCommand('zed.getConsoleHistory', async () => {
const session = await positron.runtime.getForegroundSession();
if (!session) {
vscode.window.showWarningMessage('Zed: No foreground console session to read history 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 History',
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.getConsoleHistory(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 });
}),

);
}
46 changes: 46 additions & 0 deletions src/positron-dts/positron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3097,6 +3097,52 @@ 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 ConsoleHistoryEntry {
/** 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.
*
* Console history reading is governed by the `console.historyApiEnabled`
* setting, which users can disable for privacy; when it is disabled this
* call rejects rather than returning content.
*
* @param sessionId The session ID of the session to read console history
* 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 (an empty
* array when the session has run nothing yet). Rejects if the session ID
* is unknown, or if the `console.historyApiEnabled` setting is disabled.
*/
export function getConsoleHistory(sessionId: string, numberOfEntries?: number): Thenable<ConsoleHistoryEntry[]>;

/**
* 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
15 changes: 13 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,
ISerializedConsoleHistoryEntry
} 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 All @@ -22,6 +23,7 @@ import { IPathService } from '../../../services/path/common/pathService.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IRuntimeClientInstance, IRuntimeClientOutput, RuntimeClientState, RuntimeClientStatus, RuntimeClientType } from '../../../services/languageRuntime/common/languageRuntimeClientInstance.js';
import { DeferredPromise } from '../../../../base/common/async.js';
import { generateUuid } from '../../../../base/common/uuid.js';
Expand Down Expand Up @@ -54,6 +56,8 @@ 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 } from '../../../services/positronHistory/common/executionHistoryService.js';
import { getConsoleHistory } from '../../../services/positronHistory/common/helpers/sessionConsoleHistory.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 @@ -1669,7 +1673,9 @@ 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,
@IConfigurationService private readonly _configurationService: IConfigurationService
) {
// 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 @@ -2004,6 +2010,11 @@ export class MainThreadLanguageRuntime
return querySessionTables(this._positronVariablesService, sessionId, accessKeys, queryTypes);
}

async $getConsoleHistory(sessionId: string, numberOfEntries?: number): Promise<ISerializedConsoleHistoryEntry[]> {
return getConsoleHistory(
this._executionHistoryService, this._runtimeSessionService, this._configurationService, 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);
},
getConsoleHistory(sessionId: string, numberOfEntries?: number): Thenable<positron.runtime.ConsoleHistoryEntry[]> {
return extHostLanguageRuntime.getConsoleHistory(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 @@ -105,6 +105,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>>;
$getConsoleHistory(sessionId: string, numberOfEntries?: number): Promise<ISerializedConsoleHistoryEntry[]>;
$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 @@ -369,6 +370,13 @@ export type ISerializedValidateAndExecuteCommandResult =
message?: string;
};

export interface ISerializedConsoleHistoryEntry {
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 @@ -1823,6 +1823,10 @@ export class ExtHostLanguageRuntime implements extHostProtocol.ExtHostLanguageRu
return this._proxy.$querySessionTables(sessionId, accessKeys, queryTypes);
}

public getConsoleHistory(sessionId: string, numberOfEntries?: number): Promise<extHostProtocol.ISerializedConsoleHistoryEntry[]> {
return this._proxy.$getConsoleHistory(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,57 @@ 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 IConsoleHistoryEntry {
/** 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_HISTORY_ENTRY_COUNT = 5;

/**
* Setting that controls whether extensions may read console history through the
* `positron.runtime.getConsoleHistory` API. Enabled by default; users can
* disable it when they don't want console input/output exposed to extensions.
*/
export const CONSOLE_HISTORY_API_ENABLED_KEY = 'console.historyApiEnabled';

/**
* Projects raw execution history entries down to the console history 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_HISTORY_ENTRY_COUNT}; non-positive values fall back to it.
*/
export function projectExecutionEntriesToConsoleHistory(entries: IExecutionHistoryEntry<unknown>[], numberOfEntries?: number): IConsoleHistoryEntry[] {
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_HISTORY_ENTRY_COUNT;
return projected.slice(-count);
}

/**
* Represents an input code fragment sent to a language runtime.
*/
Expand Down Expand Up @@ -180,6 +231,12 @@ const inputHistoryConfigurationNode: IConfigurationNode = {
markdownDescription: nls.localize('console.inputHistorySize', "The number of recent commands to store for each language. Set to 0 to disable history storage."),
'default': 1000,
'minimum': 0
},
[CONSOLE_HISTORY_API_ENABLED_KEY]: {
type: 'boolean',
markdownDescription: nls.localize('positron.console.historyApiEnabled', "Allow extensions to read recent console history (commands, output, and errors) through the console history API. Disable this if you don't want console content exposed to extensions."),
'default': true,
scope: ConfigurationScope.WINDOW
}
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2026 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IRuntimeSessionService } from '../../../runtimeSession/common/runtimeSessionService.js';
import { CONSOLE_HISTORY_API_ENABLED_KEY, IConsoleHistoryEntry, IExecutionHistoryService, projectExecutionEntriesToConsoleHistory } from '../executionHistoryService.js';

/**
* Get the recent console history for a session: the code fragments that have
* run, each paired with its output and any error.
*
* Two guards run before any history is read, both throwing a descriptive error:
*
* - The {@link CONSOLE_HISTORY_API_ENABLED_KEY} privacy setting must be enabled
* (it is by default); a user can disable it to keep console content from
* being exposed to extensions.
* - The session must exist. Validating it keeps the error surface consistent
* with the sibling session-scoped read APIs (`getSessionVariables` /
* `querySessionTables`) and avoids silently allocating a permanent, empty
* execution history through the create-on-read path of
* {@link IExecutionHistoryService.getExecutionEntries} when handed an
* untrusted session ID.
*
* The setting is read live so a mid-session toggle takes effect immediately.
*
* @param executionHistoryService The execution history service.
* @param runtimeSessionService The runtime session service, used to validate the session.
* @param configurationService The configuration service, used to read the privacy setting.
* @param sessionId The runtime session to read console history for.
* @param numberOfEntries The number of most recent entries to return.
* @returns The projected console history entries, oldest first.
*/
export function getConsoleHistory(
executionHistoryService: IExecutionHistoryService,
runtimeSessionService: IRuntimeSessionService,
configurationService: IConfigurationService,
sessionId: string,
numberOfEntries?: number): IConsoleHistoryEntry[] {
if (configurationService.getValue<boolean>(CONSOLE_HISTORY_API_ENABLED_KEY) === false) {
throw new Error(`Console history is unavailable because the "${CONSOLE_HISTORY_API_ENABLED_KEY}" setting is disabled.`);
}
if (!runtimeSessionService.getSession(sessionId)) {
throw new Error(`No such session: ${sessionId}`);
}
return projectExecutionEntriesToConsoleHistory(
executionHistoryService.getExecutionEntries(sessionId), numberOfEntries);
}
Loading
Loading