From 276a5e007db1b71885ae5a66dc3f76330c152f9a Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Wed, 29 Jul 2026 10:09:54 -0500 Subject: [PATCH 1/9] Add getConsoleContent tool for reading recent console history Adds a read-only Positron Assistant tool that returns recent console executions (input, output, and any error) for a session, letting the model inspect what has already run without executing code. Because it carries no requires-actions tag, it stays available in Ask mode while executeCode remains gated to agent mode. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../positron/positronToolFilter.vitest.ts | 10 +++ .../browser/tools/positronAssistantTools.ts | 90 +++++++++++++++++++ .../common/positronAssistantToolNames.ts | 1 + .../browser/positronAssistantTools.vitest.ts | 70 ++++++++++++++- 4 files changed, 170 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/test/common/positron/positronToolFilter.vitest.ts b/src/vs/workbench/api/test/common/positron/positronToolFilter.vitest.ts index 412c1a59aea4..b5357892008d 100644 --- a/src/vs/workbench/api/test/common/positron/positronToolFilter.vitest.ts +++ b/src/vs/workbench/api/test/common/positron/positronToolFilter.vitest.ts @@ -76,6 +76,16 @@ describe('getEnabledTools', () => { expect(getEnabledTools(inEditor, tools, true)).toEqual([]); }); + it('keeps read-only getConsoleContent available in Ask mode while excluding executeCode', () => { + const tools = [ + tool('executeCode', ['positron-assistant', 'requires-session']), + tool('getConsoleContent', ['positron-assistant', 'requires-session']), + ]; + // Ask mode is the Chat participant (not agent mode); executeCode is + // gated to agent mode, but the read-only console tool stays enabled. + expect(getEnabledTools(request(), tools, true, ParticipantID.Chat)).toEqual(['getConsoleContent']); + }); + it('disables all tools for the terminal participant', () => { const tools = [tool('foo')]; expect(getEnabledTools(request(), tools, true, ParticipantID.Terminal)).toEqual([]); diff --git a/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts b/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts index 8d9e60a99f1a..628975114839 100644 --- a/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts +++ b/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts @@ -18,6 +18,7 @@ import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocatio import { RuntimeCodeExecutionMode, RuntimeErrorBehavior } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; import { IPositronConsoleService } from '../../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; import { CodeAttributionSource, IConsoleCodeAttribution } from '../../../../services/positronConsole/common/positronConsoleCodeExecution.js'; +import { ExecutionEntryType, IExecutionHistoryError, IExecutionHistoryService } from '../../../../services/positronHistory/common/executionHistoryService.js'; import { getSessionVariables, querySessionTables } from '../../../../services/positronVariables/common/helpers/sessionVariableQueries.js'; import { IPositronVariablesService } from '../../../../services/positronVariables/common/interfaces/positronVariablesService.js'; import { IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; @@ -225,6 +226,94 @@ export class GetTableSummaryTool implements IToolImpl { //#endregion +//#region getConsoleContent + +/** Input for the get-console-content tool. */ +interface IGetConsoleContentToolInput { + sessionIdentifier?: string; + numberOfEntries?: number; +} + +/** Default number of recent console entries returned when the model doesn't ask for a specific count. */ +const DEFAULT_CONSOLE_ENTRY_COUNT = 10; + +const getConsoleContentToolData: IToolData = { + id: PositronAssistantToolName.GetConsoleContent, + toolReferenceName: PositronAssistantToolName.GetConsoleContent, + source: ToolDataSource.Internal, + when: aiEnabledWhen, + canBeReferencedInPrompt: true, + icon: ThemeIcon.fromId('terminal'), + tags: positronSessionToolTags, + displayName: localize('positron.assistant.tool.getConsoleContent.displayName', "Get Console Content"), + userDescription: localize('positron.assistant.tool.getConsoleContent.userDescription', "Read recent console commands and their output."), + modelDescription: 'Retrieve recent console history for a session: the commands that have already run, each paired with its output and any error. This is a read-only tool that does not execute any code. Use it to inspect what has already happened in the console (for example, a recent error or a printed value) without running anything.', + inputSchema: { + type: 'object', + properties: { + sessionIdentifier: { + type: 'string', + description: 'The identifier of the session to read console content from. Optional; defaults to the active session.', + }, + numberOfEntries: { + type: 'number', + description: `The number of most recent console entries to return. Optional; defaults to ${DEFAULT_CONSOLE_ENTRY_COUNT}. Use a larger value to look further back in the console history.`, + }, + }, + }, +}; + +/** A single console entry projected to the fields relevant to the model. */ +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; +} + +export class GetConsoleContentTool implements IToolImpl { + constructor( + @IRuntimeSessionService private readonly _runtimeSessionService: IRuntimeSessionService, + @IExecutionHistoryService private readonly _executionHistoryService: IExecutionHistoryService, + ) { } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { + const input = invocation.parameters as IGetConsoleContentToolInput; + + const sessionId = resolveSessionId(this._runtimeSessionService, input.sessionIdentifier); + if (!sessionId) { + return createToolSimpleTextResult('No console session is available to read content from.'); + } + + // Only completed code executions are of interest: skip the startup banner + // and any entry without input (e.g. output recorded outside an execution). + // Note the history service does not record errors that occur outside an + // execution, nor in-flight executions that have not yet completed. + const entries = this._executionHistoryService.getExecutionEntries(sessionId) + .filter(entry => entry.outputType === ExecutionEntryType.Execution && entry.input); + + // Return the most recent entries, oldest first so the model reads them in + // chronological order. + const count = input.numberOfEntries && input.numberOfEntries > 0 + ? input.numberOfEntries + : DEFAULT_CONSOLE_ENTRY_COUNT; + const recent: IConsoleContentEntry[] = entries.slice(-count).map(entry => ({ + input: entry.input, + output: typeof entry.output === 'string' ? entry.output : String(entry.output ?? ''), + error: entry.error, + when: entry.when, + })); + + return createToolSimpleTextResult(JSON.stringify(recent)); + } +} + +//#endregion + //#region executeCode const executeCodeToolData: IToolData = { @@ -354,6 +443,7 @@ export class PositronAssistantToolsContribution extends Disposable implements IW super(); this._register(toolsService.registerTool(executeCodeToolData, instantiationService.createInstance(ExecuteCodeTool))); + this._register(toolsService.registerTool(getConsoleContentToolData, instantiationService.createInstance(GetConsoleContentTool))); this._register(toolsService.registerTool(getPlotToolData, instantiationService.createInstance(GetPlotTool))); this._register(toolsService.registerTool(inspectVariablesToolData, instantiationService.createInstance(InspectVariablesTool))); this._register(toolsService.registerTool(getTableSummaryToolData, instantiationService.createInstance(GetTableSummaryTool))); diff --git a/src/vs/workbench/contrib/positronAssistant/common/positronAssistantToolNames.ts b/src/vs/workbench/contrib/positronAssistant/common/positronAssistantToolNames.ts index 0f7b091ed38a..b9888be12f02 100644 --- a/src/vs/workbench/contrib/positronAssistant/common/positronAssistantToolNames.ts +++ b/src/vs/workbench/contrib/positronAssistant/common/positronAssistantToolNames.ts @@ -12,6 +12,7 @@ */ export enum PositronAssistantToolName { ExecuteCode = 'executeCode', + GetConsoleContent = 'getConsoleContent', GetTableSummary = 'getTableSummary', GetPlot = 'getPlot', InspectVariables = 'inspectVariables', diff --git a/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantTools.vitest.ts b/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantTools.vitest.ts index b70e8cb18184..7bfe9bb5d290 100644 --- a/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantTools.vitest.ts +++ b/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantTools.vitest.ts @@ -14,11 +14,12 @@ import { ILanguageRuntimeMessageResult, ILanguageRuntimeMetadata } from '../../. import { VariablesClientInstance } from '../../../../services/languageRuntime/common/languageRuntimeVariablesClient.js'; import { InspectedVariable, PositronVariablesComm, QueryTableSummaryResult, Variable, VariableList } from '../../../../services/languageRuntime/common/positronVariablesComm.js'; import { IPositronConsoleService } from '../../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; +import { ExecutionEntryType, IExecutionHistoryEntry, IExecutionHistoryService } from '../../../../services/positronHistory/common/executionHistoryService.js'; import { IPositronVariablesInstance } from '../../../../services/positronVariables/common/interfaces/positronVariablesInstance.js'; import { IPositronVariablesService } from '../../../../services/positronVariables/common/interfaces/positronVariablesService.js'; import { ILanguageRuntimeSession, IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; import { CountTokensCallback, IToolInvocation, IToolResult, ToolProgress } from '../../../chat/common/tools/languageModelToolsService.js'; -import { ExecuteCodeTool, GetPlotTool, GetTableSummaryTool, InspectVariablesTool } from '../../browser/tools/positronAssistantTools.js'; +import { ExecuteCodeTool, GetConsoleContentTool, GetPlotTool, GetTableSummaryTool, InspectVariablesTool } from '../../browser/tools/positronAssistantTools.js'; import { IPositronAssistantService } from '../../common/interfaces/positronAssistantService.js'; const SESSION_ID = 'session-1'; @@ -153,6 +154,73 @@ describe('GetTableSummaryTool', () => { }); }); +describe('GetConsoleContentTool', () => { + /** Build an execution history entry, defaulting to a completed code execution. */ + function entry(overrides: Partial>): IExecutionHistoryEntry { + return { + id: 'id', + when: 0, + prompt: '', + input: '', + outputType: ExecutionEntryType.Execution, + output: '', + durationMs: 0, + ...overrides, + }; + } + + const foreground = stubInterface({ + foregroundSession: stubInterface({ sessionId: SESSION_ID }), + }); + + function invoke(runtimeSessionService: IRuntimeSessionService, entries: IExecutionHistoryEntry[], parameters: Record = {}): Promise { + const executionHistoryService = stubInterface({ getExecutionEntries: () => entries }); + const tool = new GetConsoleContentTool(runtimeSessionService, executionHistoryService); + return tool.invoke(invocation(parameters), countTokens, progress, CancellationToken.None); + } + + it('reports a descriptive message when no session can be resolved', async () => { + const runtimeSessionService = stubInterface({ foregroundSession: undefined }); + + expect(textOf(await invoke(runtimeSessionService, []))).toBe('No console session is available to read content from.'); + }); + + it('returns recent execution entries paired with output and error, oldest first', async () => { + 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(JSON.parse(textOf(await invoke(foreground, 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('returns only the most recent entries up to numberOfEntries', async () => { + const entries = [1, 2, 3, 4, 5].map(n => entry({ input: `cmd${n}`, output: `out${n}`, when: n })); + + expect(JSON.parse(textOf(await invoke(foreground, entries, { numberOfEntries: 2 })))).toEqual([ + { input: 'cmd4', output: 'out4', when: 4 }, + { input: 'cmd5', output: 'out5', when: 5 }, + ]); + }); + + it('excludes the startup banner and entries recorded without input', async () => { + const entries = [ + entry({ outputType: ExecutionEntryType.Startup, input: '', output: 'banner', when: 1 }), + entry({ input: '', output: 'orphan output', when: 2 }), + entry({ input: 'real()', output: 'ok', when: 3 }), + ]; + + expect(JSON.parse(textOf(await invoke(foreground, entries)))).toEqual([ + { input: 'real()', output: 'ok', when: 3 }, + ]); + }); +}); + describe('ExecuteCodeTool', () => { const disposables = ensureNoLeakedDisposables(); From 1dec3f31b02a0630ab3411a3a8ec84c6d6141fac Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Wed, 29 Jul 2026 10:32:19 -0500 Subject: [PATCH 2/9] Reduce default console entry count to 5 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../positronAssistant/browser/tools/positronAssistantTools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts b/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts index 628975114839..144028069fff 100644 --- a/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts +++ b/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts @@ -235,7 +235,7 @@ interface IGetConsoleContentToolInput { } /** Default number of recent console entries returned when the model doesn't ask for a specific count. */ -const DEFAULT_CONSOLE_ENTRY_COUNT = 10; +const DEFAULT_CONSOLE_ENTRY_COUNT = 5; const getConsoleContentToolData: IToolData = { id: PositronAssistantToolName.GetConsoleContent, From 7b4b0084ca0858c488ac9ca668a2d4d8b729f5a3 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Wed, 29 Jul 2026 13:37:17 -0500 Subject: [PATCH 3/9] Add positron.ai.getConsoleContent extension API Exposes recent console history to extensions via a new positron.ai.getConsoleContent(sessionId?) function, so Posit Assistant can read console content through a native tool rather than the vscode.lm tool allowlist (the pattern the assistant is moving away from). The reading/projection logic is extracted into a shared helper, projectExecutionEntriesToConsoleContent, in the execution history service and reused by both the existing core getConsoleContent tool (for Copilot Chat) and the new extension API (for the native Posit Assistant tool), so the two stay in sync. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/positron-dts/positron.d.ts | 39 +++++++++++++++++++ .../browser/positron/mainThreadAiFeatures.ts | 18 ++++++++- .../positron/extHost.positron.api.impl.ts | 3 ++ .../positron/extHost.positron.protocol.ts | 8 ++++ .../api/common/positron/extHostAiFeatures.ts | 6 +++ .../browser/tools/positronAssistantTools.ts | 32 ++++----------- .../common/executionHistoryService.ts | 33 ++++++++++++++++ 7 files changed, 113 insertions(+), 26 deletions(-) diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index ff55c6363b77..313ec8e274b3 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -3882,6 +3882,45 @@ declare module 'positron' { commandId: string, args?: unknown[] ): Thenable; + + /** + * 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; + } + + /** + * Returns the recent console history for a runtime 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 identifier of the session to read console content + * from. Defaults to the foreground session when omitted. + * @returns A Thenable that resolves to the console entries, or an empty + * array when no session is available. + */ + export function getConsoleContent(sessionId?: string): Thenable; } /** diff --git a/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts b/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts index 582dbd8a8bd1..9edf9f216caa 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts @@ -17,9 +17,10 @@ import { IChatRequestData, IGenerateAssistantPromptRequest, IPositronAssistantCo import { extHostNamedCustomer, IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IChatProgressDto } from '../../common/extHost.protocol.js'; -import { ExtHostAiFeaturesShape, ExtHostPositronContext, ISerializedAgentCommand, ISerializedValidateAndExecuteCommandResult, MainPositronContext, MainThreadAiFeaturesShape } from '../../common/positron/extHost.positron.protocol.js'; +import { ExtHostAiFeaturesShape, ExtHostPositronContext, ISerializedAgentCommand, ISerializedConsoleContentEntry, ISerializedValidateAndExecuteCommandResult, MainPositronContext, MainThreadAiFeaturesShape } from '../../common/positron/extHost.positron.protocol.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IRuntimeSessionService } from '../../../services/runtimeSession/common/runtimeSessionService.js'; +import { IExecutionHistoryService, projectExecutionEntriesToConsoleContent } from '../../../services/positronHistory/common/executionHistoryService.js'; import { ChatModeKind } from '../../../contrib/chat/common/constants.js'; import { PromptRenderer } from '../../../contrib/positronAssistant/browser/prompts/promptRenderer.js'; import { getPositronContextPrompts } from '../../../contrib/positronAssistant/browser/prompts/positronContextPrompts.js'; @@ -44,6 +45,7 @@ export class MainThreadAiFeatures extends Disposable implements MainThreadAiFeat @IRuntimeSessionService private readonly _runtimeSessionService: IRuntimeSessionService, @IFileService private readonly _fileService: IFileService, @IAgentAllowedCommandsService private readonly _agentAllowedCommandsService: IAgentAllowedCommandsService, + @IExecutionHistoryService private readonly _executionHistoryService: IExecutionHistoryService, ) { super(); // Create the proxy for the extension host. @@ -287,4 +289,18 @@ export class MainThreadAiFeatures extends Disposable implements MainThreadAiFeat ): Promise { return this._agentAllowedCommandsService.validateAndExecute(commandId, args); } + + /** + * Return the recent console history for a session (or the foreground + * session when none is given), projected to the fields a model needs. + */ + async $getConsoleContent(sessionId: string | undefined): Promise { + const resolvedSessionId = sessionId ?? this._runtimeSessionService.foregroundSession?.sessionId; + if (!resolvedSessionId) { + return []; + } + + return projectExecutionEntriesToConsoleContent( + this._executionHistoryService.getExecutionEntries(resolvedSessionId)); + } } diff --git a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts index 18e26a3d69b4..4c15a8acf6d5 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts @@ -538,6 +538,9 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce ): Thenable { return extHostAiFeatures.validateAndExecuteCommand(commandId, args); }, + getConsoleContent(sessionId?: string): Thenable { + return extHostAiFeatures.getConsoleContent(sessionId); + }, LanguageModelAutoconfigureType: extHostTypes.LanguageModelAutoconfigureType }; diff --git a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts index 044a1b447ade..9d43314d9f5f 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts @@ -355,6 +355,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; $unregisterChatAgent(id: string): void; @@ -379,6 +386,7 @@ export interface MainThreadAiFeaturesShape { commandId: string, args: unknown[] | undefined, ): Promise; + $getConsoleContent(sessionId: string | undefined): Promise; } export interface ExtHostAiFeaturesShape { diff --git a/src/vs/workbench/api/common/positron/extHostAiFeatures.ts b/src/vs/workbench/api/common/positron/extHostAiFeatures.ts index 1d43d6627f52..178a750c693f 100644 --- a/src/vs/workbench/api/common/positron/extHostAiFeatures.ts +++ b/src/vs/workbench/api/common/positron/extHostAiFeatures.ts @@ -199,4 +199,10 @@ export class ExtHostAiFeatures implements extHostProtocol.ExtHostAiFeaturesShape return this._proxy.$validateAndExecuteCommand(commandId, args); } + async getConsoleContent( + sessionId: string | undefined, + ): Promise { + return this._proxy.$getConsoleContent(sessionId); + } + } diff --git a/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts b/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts index 144028069fff..e54b47661bfc 100644 --- a/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts +++ b/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts @@ -18,7 +18,7 @@ import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocatio import { RuntimeCodeExecutionMode, RuntimeErrorBehavior } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; import { IPositronConsoleService } from '../../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; import { CodeAttributionSource, IConsoleCodeAttribution } from '../../../../services/positronConsole/common/positronConsoleCodeExecution.js'; -import { ExecutionEntryType, IExecutionHistoryError, IExecutionHistoryService } from '../../../../services/positronHistory/common/executionHistoryService.js'; +import { IExecutionHistoryService, projectExecutionEntriesToConsoleContent } from '../../../../services/positronHistory/common/executionHistoryService.js'; import { getSessionVariables, querySessionTables } from '../../../../services/positronVariables/common/helpers/sessionVariableQueries.js'; import { IPositronVariablesService } from '../../../../services/positronVariables/common/interfaces/positronVariablesService.js'; import { IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; @@ -263,18 +263,6 @@ const getConsoleContentToolData: IToolData = { }, }; -/** A single console entry projected to the fields relevant to the model. */ -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; -} - export class GetConsoleContentTool implements IToolImpl { constructor( @IRuntimeSessionService private readonly _runtimeSessionService: IRuntimeSessionService, @@ -289,24 +277,18 @@ export class GetConsoleContentTool implements IToolImpl { return createToolSimpleTextResult('No console session is available to read content from.'); } - // Only completed code executions are of interest: skip the startup banner - // and any entry without input (e.g. output recorded outside an execution). - // Note the history service does not record errors that occur outside an - // execution, nor in-flight executions that have not yet completed. - const entries = this._executionHistoryService.getExecutionEntries(sessionId) - .filter(entry => entry.outputType === ExecutionEntryType.Execution && entry.input); + // Project to completed code executions only (see helper); the history + // service does not record errors that occur outside an execution, nor + // in-flight executions that have not yet completed. + const entries = projectExecutionEntriesToConsoleContent( + this._executionHistoryService.getExecutionEntries(sessionId)); // Return the most recent entries, oldest first so the model reads them in // chronological order. const count = input.numberOfEntries && input.numberOfEntries > 0 ? input.numberOfEntries : DEFAULT_CONSOLE_ENTRY_COUNT; - const recent: IConsoleContentEntry[] = entries.slice(-count).map(entry => ({ - input: entry.input, - output: typeof entry.output === 'string' ? entry.output : String(entry.output ?? ''), - error: entry.error, - when: entry.when, - })); + const recent = entries.slice(-count); return createToolSimpleTextResult(JSON.stringify(recent)); } diff --git a/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts b/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts index 2ae119f8fdd2..9aff7f7a8320 100644 --- a/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts +++ b/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts @@ -70,6 +70,39 @@ 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; +} + +/** + * 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. Entries are + * returned in their stored order (oldest first). + */ +export function projectExecutionEntriesToConsoleContent(entries: IExecutionHistoryEntry[]): IConsoleContentEntry[] { + return 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, + })); +} + /** * Represents an input code fragment sent to a language runtime. */ From d9682828c9c8cdecacb1f4a06b115b1e06f87fe3 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Wed, 29 Jul 2026 13:45:12 -0500 Subject: [PATCH 4/9] Remove core vscode.lm getConsoleContent tool The core getConsoleContent tool was exposed to chat clients via vscode.lm. Now that Posit Assistant reads console history natively through the positron.ai.getConsoleContent extension API, this tool is vestigial and contradicts the direction of moving off vscode.lm tools, so remove it (tool data, class, registration, and tool-name enum entry). The projection logic (projectExecutionEntriesToConsoleContent) stays in the execution history service, still used by the extension API, and gains a direct unit test in place of the removed tool's tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../positron/positronToolFilter.vitest.ts | 8 +-- .../browser/tools/positronAssistantTools.ts | 72 ------------------- .../common/positronAssistantToolNames.ts | 1 - .../browser/positronAssistantTools.vitest.ts | 70 +----------------- .../common/executionHistoryService.vitest.ts | 44 +++++++++++- 5 files changed, 48 insertions(+), 147 deletions(-) diff --git a/src/vs/workbench/api/test/common/positron/positronToolFilter.vitest.ts b/src/vs/workbench/api/test/common/positron/positronToolFilter.vitest.ts index b5357892008d..b03c27f2d7ba 100644 --- a/src/vs/workbench/api/test/common/positron/positronToolFilter.vitest.ts +++ b/src/vs/workbench/api/test/common/positron/positronToolFilter.vitest.ts @@ -76,14 +76,14 @@ describe('getEnabledTools', () => { expect(getEnabledTools(inEditor, tools, true)).toEqual([]); }); - it('keeps read-only getConsoleContent available in Ask mode while excluding executeCode', () => { + it('keeps a read-only session tool available in Ask mode while excluding executeCode', () => { const tools = [ tool('executeCode', ['positron-assistant', 'requires-session']), - tool('getConsoleContent', ['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 the read-only console tool stays enabled. - expect(getEnabledTools(request(), tools, true, ParticipantID.Chat)).toEqual(['getConsoleContent']); + // 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', () => { diff --git a/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts b/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts index e54b47661bfc..8d9e60a99f1a 100644 --- a/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts +++ b/src/vs/workbench/contrib/positronAssistant/browser/tools/positronAssistantTools.ts @@ -18,7 +18,6 @@ import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocatio import { RuntimeCodeExecutionMode, RuntimeErrorBehavior } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; import { IPositronConsoleService } from '../../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; import { CodeAttributionSource, IConsoleCodeAttribution } from '../../../../services/positronConsole/common/positronConsoleCodeExecution.js'; -import { IExecutionHistoryService, projectExecutionEntriesToConsoleContent } from '../../../../services/positronHistory/common/executionHistoryService.js'; import { getSessionVariables, querySessionTables } from '../../../../services/positronVariables/common/helpers/sessionVariableQueries.js'; import { IPositronVariablesService } from '../../../../services/positronVariables/common/interfaces/positronVariablesService.js'; import { IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; @@ -226,76 +225,6 @@ export class GetTableSummaryTool implements IToolImpl { //#endregion -//#region getConsoleContent - -/** Input for the get-console-content tool. */ -interface IGetConsoleContentToolInput { - sessionIdentifier?: string; - numberOfEntries?: number; -} - -/** Default number of recent console entries returned when the model doesn't ask for a specific count. */ -const DEFAULT_CONSOLE_ENTRY_COUNT = 5; - -const getConsoleContentToolData: IToolData = { - id: PositronAssistantToolName.GetConsoleContent, - toolReferenceName: PositronAssistantToolName.GetConsoleContent, - source: ToolDataSource.Internal, - when: aiEnabledWhen, - canBeReferencedInPrompt: true, - icon: ThemeIcon.fromId('terminal'), - tags: positronSessionToolTags, - displayName: localize('positron.assistant.tool.getConsoleContent.displayName', "Get Console Content"), - userDescription: localize('positron.assistant.tool.getConsoleContent.userDescription', "Read recent console commands and their output."), - modelDescription: 'Retrieve recent console history for a session: the commands that have already run, each paired with its output and any error. This is a read-only tool that does not execute any code. Use it to inspect what has already happened in the console (for example, a recent error or a printed value) without running anything.', - inputSchema: { - type: 'object', - properties: { - sessionIdentifier: { - type: 'string', - description: 'The identifier of the session to read console content from. Optional; defaults to the active session.', - }, - numberOfEntries: { - type: 'number', - description: `The number of most recent console entries to return. Optional; defaults to ${DEFAULT_CONSOLE_ENTRY_COUNT}. Use a larger value to look further back in the console history.`, - }, - }, - }, -}; - -export class GetConsoleContentTool implements IToolImpl { - constructor( - @IRuntimeSessionService private readonly _runtimeSessionService: IRuntimeSessionService, - @IExecutionHistoryService private readonly _executionHistoryService: IExecutionHistoryService, - ) { } - - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { - const input = invocation.parameters as IGetConsoleContentToolInput; - - const sessionId = resolveSessionId(this._runtimeSessionService, input.sessionIdentifier); - if (!sessionId) { - return createToolSimpleTextResult('No console session is available to read content from.'); - } - - // Project to completed code executions only (see helper); the history - // service does not record errors that occur outside an execution, nor - // in-flight executions that have not yet completed. - const entries = projectExecutionEntriesToConsoleContent( - this._executionHistoryService.getExecutionEntries(sessionId)); - - // Return the most recent entries, oldest first so the model reads them in - // chronological order. - const count = input.numberOfEntries && input.numberOfEntries > 0 - ? input.numberOfEntries - : DEFAULT_CONSOLE_ENTRY_COUNT; - const recent = entries.slice(-count); - - return createToolSimpleTextResult(JSON.stringify(recent)); - } -} - -//#endregion - //#region executeCode const executeCodeToolData: IToolData = { @@ -425,7 +354,6 @@ export class PositronAssistantToolsContribution extends Disposable implements IW super(); this._register(toolsService.registerTool(executeCodeToolData, instantiationService.createInstance(ExecuteCodeTool))); - this._register(toolsService.registerTool(getConsoleContentToolData, instantiationService.createInstance(GetConsoleContentTool))); this._register(toolsService.registerTool(getPlotToolData, instantiationService.createInstance(GetPlotTool))); this._register(toolsService.registerTool(inspectVariablesToolData, instantiationService.createInstance(InspectVariablesTool))); this._register(toolsService.registerTool(getTableSummaryToolData, instantiationService.createInstance(GetTableSummaryTool))); diff --git a/src/vs/workbench/contrib/positronAssistant/common/positronAssistantToolNames.ts b/src/vs/workbench/contrib/positronAssistant/common/positronAssistantToolNames.ts index b9888be12f02..0f7b091ed38a 100644 --- a/src/vs/workbench/contrib/positronAssistant/common/positronAssistantToolNames.ts +++ b/src/vs/workbench/contrib/positronAssistant/common/positronAssistantToolNames.ts @@ -12,7 +12,6 @@ */ export enum PositronAssistantToolName { ExecuteCode = 'executeCode', - GetConsoleContent = 'getConsoleContent', GetTableSummary = 'getTableSummary', GetPlot = 'getPlot', InspectVariables = 'inspectVariables', diff --git a/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantTools.vitest.ts b/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantTools.vitest.ts index 7bfe9bb5d290..b70e8cb18184 100644 --- a/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantTools.vitest.ts +++ b/src/vs/workbench/contrib/positronAssistant/test/browser/positronAssistantTools.vitest.ts @@ -14,12 +14,11 @@ import { ILanguageRuntimeMessageResult, ILanguageRuntimeMetadata } from '../../. import { VariablesClientInstance } from '../../../../services/languageRuntime/common/languageRuntimeVariablesClient.js'; import { InspectedVariable, PositronVariablesComm, QueryTableSummaryResult, Variable, VariableList } from '../../../../services/languageRuntime/common/positronVariablesComm.js'; import { IPositronConsoleService } from '../../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; -import { ExecutionEntryType, IExecutionHistoryEntry, IExecutionHistoryService } from '../../../../services/positronHistory/common/executionHistoryService.js'; import { IPositronVariablesInstance } from '../../../../services/positronVariables/common/interfaces/positronVariablesInstance.js'; import { IPositronVariablesService } from '../../../../services/positronVariables/common/interfaces/positronVariablesService.js'; import { ILanguageRuntimeSession, IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; import { CountTokensCallback, IToolInvocation, IToolResult, ToolProgress } from '../../../chat/common/tools/languageModelToolsService.js'; -import { ExecuteCodeTool, GetConsoleContentTool, GetPlotTool, GetTableSummaryTool, InspectVariablesTool } from '../../browser/tools/positronAssistantTools.js'; +import { ExecuteCodeTool, GetPlotTool, GetTableSummaryTool, InspectVariablesTool } from '../../browser/tools/positronAssistantTools.js'; import { IPositronAssistantService } from '../../common/interfaces/positronAssistantService.js'; const SESSION_ID = 'session-1'; @@ -154,73 +153,6 @@ describe('GetTableSummaryTool', () => { }); }); -describe('GetConsoleContentTool', () => { - /** Build an execution history entry, defaulting to a completed code execution. */ - function entry(overrides: Partial>): IExecutionHistoryEntry { - return { - id: 'id', - when: 0, - prompt: '', - input: '', - outputType: ExecutionEntryType.Execution, - output: '', - durationMs: 0, - ...overrides, - }; - } - - const foreground = stubInterface({ - foregroundSession: stubInterface({ sessionId: SESSION_ID }), - }); - - function invoke(runtimeSessionService: IRuntimeSessionService, entries: IExecutionHistoryEntry[], parameters: Record = {}): Promise { - const executionHistoryService = stubInterface({ getExecutionEntries: () => entries }); - const tool = new GetConsoleContentTool(runtimeSessionService, executionHistoryService); - return tool.invoke(invocation(parameters), countTokens, progress, CancellationToken.None); - } - - it('reports a descriptive message when no session can be resolved', async () => { - const runtimeSessionService = stubInterface({ foregroundSession: undefined }); - - expect(textOf(await invoke(runtimeSessionService, []))).toBe('No console session is available to read content from.'); - }); - - it('returns recent execution entries paired with output and error, oldest first', async () => { - 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(JSON.parse(textOf(await invoke(foreground, 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('returns only the most recent entries up to numberOfEntries', async () => { - const entries = [1, 2, 3, 4, 5].map(n => entry({ input: `cmd${n}`, output: `out${n}`, when: n })); - - expect(JSON.parse(textOf(await invoke(foreground, entries, { numberOfEntries: 2 })))).toEqual([ - { input: 'cmd4', output: 'out4', when: 4 }, - { input: 'cmd5', output: 'out5', when: 5 }, - ]); - }); - - it('excludes the startup banner and entries recorded without input', async () => { - const entries = [ - entry({ outputType: ExecutionEntryType.Startup, input: '', output: 'banner', when: 1 }), - entry({ input: '', output: 'orphan output', when: 2 }), - entry({ input: 'real()', output: 'ok', when: 3 }), - ]; - - expect(JSON.parse(textOf(await invoke(foreground, entries)))).toEqual([ - { input: 'real()', output: 'ok', when: 3 }, - ]); - }); -}); - describe('ExecuteCodeTool', () => { const disposables = ensureNoLeakedDisposables(); diff --git a/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts b/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts index 64b8a70757a3..ac529603faca 100644 --- a/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts +++ b/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts @@ -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 } 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'; @@ -1061,3 +1061,45 @@ 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 { + 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 }, + ]); + }); +}); From 2647644e51c2353fc16da0badf54b3e9d798b65c Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Wed, 29 Jul 2026 15:00:11 -0500 Subject: [PATCH 5/9] Move getConsoleContent API to positron.runtime namespace Relocate getConsoleContent from positron.ai to positron.runtime, where it sits alongside the other session-scoped read APIs getSessionVariables and querySessionTables (which back the equivalent inspect tools). Reading a session's console history is a runtime-state concern, not an AI-specific one, so it belongs with the runtime session APIs rather than the AI feature surface. The signature now takes a required sessionId to match its siblings; the caller resolves the foreground session. Plumbing moves from the AiFeatures extHost/mainThread pair to the LanguageRuntime pair accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/positron-dts/positron.d.ts | 78 +++++++++---------- .../browser/positron/mainThreadAiFeatures.ts | 18 +---- .../positron/mainThreadLanguageRuntime.ts | 12 ++- .../positron/extHost.positron.api.impl.ts | 6 +- .../positron/extHost.positron.protocol.ts | 2 +- .../api/common/positron/extHostAiFeatures.ts | 6 -- .../common/positron/extHostLanguageRuntime.ts | 4 + 7 files changed, 58 insertions(+), 68 deletions(-) diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 313ec8e274b3..14241ce29923 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -3069,6 +3069,45 @@ declare module 'positron' { queryTypes: Array): Thenable>; + /** + * 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. + * @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): Thenable; + /** * 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 @@ -3882,45 +3921,6 @@ declare module 'positron' { commandId: string, args?: unknown[] ): Thenable; - - /** - * 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; - } - - /** - * Returns the recent console history for a runtime 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 identifier of the session to read console content - * from. Defaults to the foreground session when omitted. - * @returns A Thenable that resolves to the console entries, or an empty - * array when no session is available. - */ - export function getConsoleContent(sessionId?: string): Thenable; } /** diff --git a/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts b/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts index 9edf9f216caa..582dbd8a8bd1 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts @@ -17,10 +17,9 @@ import { IChatRequestData, IGenerateAssistantPromptRequest, IPositronAssistantCo import { extHostNamedCustomer, IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IChatProgressDto } from '../../common/extHost.protocol.js'; -import { ExtHostAiFeaturesShape, ExtHostPositronContext, ISerializedAgentCommand, ISerializedConsoleContentEntry, ISerializedValidateAndExecuteCommandResult, MainPositronContext, MainThreadAiFeaturesShape } from '../../common/positron/extHost.positron.protocol.js'; +import { ExtHostAiFeaturesShape, ExtHostPositronContext, ISerializedAgentCommand, ISerializedValidateAndExecuteCommandResult, MainPositronContext, MainThreadAiFeaturesShape } from '../../common/positron/extHost.positron.protocol.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IRuntimeSessionService } from '../../../services/runtimeSession/common/runtimeSessionService.js'; -import { IExecutionHistoryService, projectExecutionEntriesToConsoleContent } from '../../../services/positronHistory/common/executionHistoryService.js'; import { ChatModeKind } from '../../../contrib/chat/common/constants.js'; import { PromptRenderer } from '../../../contrib/positronAssistant/browser/prompts/promptRenderer.js'; import { getPositronContextPrompts } from '../../../contrib/positronAssistant/browser/prompts/positronContextPrompts.js'; @@ -45,7 +44,6 @@ export class MainThreadAiFeatures extends Disposable implements MainThreadAiFeat @IRuntimeSessionService private readonly _runtimeSessionService: IRuntimeSessionService, @IFileService private readonly _fileService: IFileService, @IAgentAllowedCommandsService private readonly _agentAllowedCommandsService: IAgentAllowedCommandsService, - @IExecutionHistoryService private readonly _executionHistoryService: IExecutionHistoryService, ) { super(); // Create the proxy for the extension host. @@ -289,18 +287,4 @@ export class MainThreadAiFeatures extends Disposable implements MainThreadAiFeat ): Promise { return this._agentAllowedCommandsService.validateAndExecute(commandId, args); } - - /** - * Return the recent console history for a session (or the foreground - * session when none is given), projected to the fields a model needs. - */ - async $getConsoleContent(sessionId: string | undefined): Promise { - const resolvedSessionId = sessionId ?? this._runtimeSessionService.foregroundSession?.sessionId; - if (!resolvedSessionId) { - return []; - } - - return projectExecutionEntriesToConsoleContent( - this._executionHistoryService.getExecutionEntries(resolvedSessionId)); - } } diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index 49eb8eeda7a0..48defeb07ad3 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -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'; @@ -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'; @@ -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 @@ -1982,6 +1985,11 @@ export class MainThreadLanguageRuntime return querySessionTables(this._positronVariablesService, sessionId, accessKeys, queryTypes); } + async $getConsoleContent(sessionId: string): Promise { + return projectExecutionEntriesToConsoleContent( + this._executionHistoryService.getExecutionEntries(sessionId)); + } + /** * Emit a performance mark for a given extension. This is used to track the * timing of startup actions that happen in extensions. diff --git a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts index 4c15a8acf6d5..8a167c392b0e 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts @@ -179,6 +179,9 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce Thenable> { return extHostLanguageRuntime.querySessionTables(sessionId, accessKeys, queryTypes); }, + getConsoleContent(sessionId: string): Thenable { + return extHostLanguageRuntime.getConsoleContent(sessionId); + }, registerClientHandler(handler: positron.RuntimeClientHandler): vscode.Disposable { return extHostLanguageRuntime.registerClientHandler(handler); }, @@ -538,9 +541,6 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce ): Thenable { return extHostAiFeatures.validateAndExecuteCommand(commandId, args); }, - getConsoleContent(sessionId?: string): Thenable { - return extHostAiFeatures.getConsoleContent(sessionId); - }, LanguageModelAutoconfigureType: extHostTypes.LanguageModelAutoconfigureType }; diff --git a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts index 9d43314d9f5f..af21188cc627 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts @@ -92,6 +92,7 @@ export interface MainThreadLanguageRuntimeShape extends IDisposable { $getSessionWorkingDirectory(sessionId?: string): Promise; $getSessionVariables(sessionId: string, accessKeys?: Array>): Promise>>; $querySessionTables(sessionId: string, accessKeys: Array>, queryTypes: Array): Promise>; + $getConsoleContent(sessionId: string): Promise; $callMethod(sessionId: string, method: string, args: unknown[]): Thenable; $emitPerfMark(extensionId: string, name: string): void; $emitLanguageRuntimeMessage(sessionId: string, handled: boolean, message: SerializableObjectWithBuffers): void; @@ -386,7 +387,6 @@ export interface MainThreadAiFeaturesShape { commandId: string, args: unknown[] | undefined, ): Promise; - $getConsoleContent(sessionId: string | undefined): Promise; } export interface ExtHostAiFeaturesShape { diff --git a/src/vs/workbench/api/common/positron/extHostAiFeatures.ts b/src/vs/workbench/api/common/positron/extHostAiFeatures.ts index 178a750c693f..1d43d6627f52 100644 --- a/src/vs/workbench/api/common/positron/extHostAiFeatures.ts +++ b/src/vs/workbench/api/common/positron/extHostAiFeatures.ts @@ -199,10 +199,4 @@ export class ExtHostAiFeatures implements extHostProtocol.ExtHostAiFeaturesShape return this._proxy.$validateAndExecuteCommand(commandId, args); } - async getConsoleContent( - sessionId: string | undefined, - ): Promise { - return this._proxy.$getConsoleContent(sessionId); - } - } diff --git a/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts b/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts index 7c5ba8f51c0d..492a1ed71fab 100644 --- a/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts +++ b/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts @@ -1798,6 +1798,10 @@ export class ExtHostLanguageRuntime implements extHostProtocol.ExtHostLanguageRu return this._proxy.$querySessionTables(sessionId, accessKeys, queryTypes); } + public getConsoleContent(sessionId: string): Promise { + return this._proxy.$getConsoleContent(sessionId); + } + /** * Interrupts an active session. * From bfdf224434c75bb1faba67253c41c55823b30a44 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Wed, 29 Jul 2026 16:10:40 -0500 Subject: [PATCH 6/9] Limit getConsoleContent to a default of 5 recent entries Add an optional numberOfEntries argument to positron.runtime.getConsoleContent, applied server-side (in the shared projection helper) so only the requested number of most recent entries crosses the extension-host boundary instead of the whole session history. Defaults to 5; non-positive values fall back to the default. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/positron-dts/positron.d.ts | 4 +++- .../positron/mainThreadLanguageRuntime.ts | 4 ++-- .../positron/extHost.positron.api.impl.ts | 4 ++-- .../positron/extHost.positron.protocol.ts | 2 +- .../common/positron/extHostLanguageRuntime.ts | 4 ++-- .../common/executionHistoryService.ts | 19 +++++++++++++++---- .../common/executionHistoryService.vitest.ts | 18 +++++++++++++++++- 7 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 14241ce29923..073ddd405e35 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -3103,10 +3103,12 @@ declare module 'positron' { * * @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): Thenable; + export function getConsoleContent(sessionId: string, numberOfEntries?: number): Thenable; /** * Register a handler for runtime client instances. This handler will be called diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index 48defeb07ad3..2ce7f2905438 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -1985,9 +1985,9 @@ export class MainThreadLanguageRuntime return querySessionTables(this._positronVariablesService, sessionId, accessKeys, queryTypes); } - async $getConsoleContent(sessionId: string): Promise { + async $getConsoleContent(sessionId: string, numberOfEntries?: number): Promise { return projectExecutionEntriesToConsoleContent( - this._executionHistoryService.getExecutionEntries(sessionId)); + this._executionHistoryService.getExecutionEntries(sessionId), numberOfEntries); } /** diff --git a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts index 8a167c392b0e..0f57fcf105e4 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts @@ -179,8 +179,8 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce Thenable> { return extHostLanguageRuntime.querySessionTables(sessionId, accessKeys, queryTypes); }, - getConsoleContent(sessionId: string): Thenable { - return extHostLanguageRuntime.getConsoleContent(sessionId); + getConsoleContent(sessionId: string, numberOfEntries?: number): Thenable { + return extHostLanguageRuntime.getConsoleContent(sessionId, numberOfEntries); }, registerClientHandler(handler: positron.RuntimeClientHandler): vscode.Disposable { return extHostLanguageRuntime.registerClientHandler(handler); diff --git a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts index af21188cc627..ccead4324aef 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts @@ -92,7 +92,7 @@ export interface MainThreadLanguageRuntimeShape extends IDisposable { $getSessionWorkingDirectory(sessionId?: string): Promise; $getSessionVariables(sessionId: string, accessKeys?: Array>): Promise>>; $querySessionTables(sessionId: string, accessKeys: Array>, queryTypes: Array): Promise>; - $getConsoleContent(sessionId: string): Promise; + $getConsoleContent(sessionId: string, numberOfEntries?: number): Promise; $callMethod(sessionId: string, method: string, args: unknown[]): Thenable; $emitPerfMark(extensionId: string, name: string): void; $emitLanguageRuntimeMessage(sessionId: string, handled: boolean, message: SerializableObjectWithBuffers): void; diff --git a/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts b/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts index 492a1ed71fab..5d6d4347fcf4 100644 --- a/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts +++ b/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts @@ -1798,8 +1798,8 @@ export class ExtHostLanguageRuntime implements extHostProtocol.ExtHostLanguageRu return this._proxy.$querySessionTables(sessionId, accessKeys, queryTypes); } - public getConsoleContent(sessionId: string): Promise { - return this._proxy.$getConsoleContent(sessionId); + public getConsoleContent(sessionId: string, numberOfEntries?: number): Promise { + return this._proxy.$getConsoleContent(sessionId, numberOfEntries); } /** diff --git a/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts b/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts index 9aff7f7a8320..5a688b89f29a 100644 --- a/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts +++ b/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts @@ -85,15 +85,23 @@ export interface IConsoleContentEntry { 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. Entries are - * returned in their stored order (oldest first). + * 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[]): IConsoleContentEntry[] { - return entries +export function projectExecutionEntriesToConsoleContent(entries: IExecutionHistoryEntry[], numberOfEntries?: number): IConsoleContentEntry[] { + const projected = entries .filter(entry => entry.outputType === ExecutionEntryType.Execution && entry.input) .map(entry => ({ input: entry.input, @@ -101,6 +109,9 @@ export function projectExecutionEntriesToConsoleContent(entries: IExecutionHisto error: entry.error, when: entry.when, })); + + const count = numberOfEntries && numberOfEntries > 0 ? numberOfEntries : DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT; + return projected.slice(-count); } /** diff --git a/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts b/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts index ac529603faca..28d3ad19c267 100644 --- a/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts +++ b/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts @@ -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, IExecutionHistoryEntry, projectExecutionEntriesToConsoleContent } 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'; @@ -1102,4 +1102,20 @@ describe('projectExecutionEntriesToConsoleContent', () => { { 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); + }); }); From 085c46611aecac3b755703d1dd9da3f5f9e47df8 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Thu, 30 Jul 2026 16:36:31 -0500 Subject: [PATCH 7/9] Add Zed command to demo getConsoleContent Adds a "Zed: Get Console Content" command to the positron-zed extension that reads the foreground session's recent console history via positron.runtime.getConsoleContent (prompting for an optional entry count) and opens the result in a JSON editor. Provides a way to exercise the new API without Posit Assistant installed. Co-Authored-By: Claude Opus 4.8 (1M context) --- extensions/positron-zed/package.json | 9 +++++++ extensions/positron-zed/src/commands.ts | 35 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/extensions/positron-zed/package.json b/extensions/positron-zed/package.json index 1c6ba4440444..73aacb74a0ac 100644 --- a/extensions/positron-zed/package.json +++ b/extensions/positron-zed/package.json @@ -74,6 +74,11 @@ "command": "zed.quartoVisualMode", "category": "Zed", "title": "Edit Quarto File in Visual Mode" + }, + { + "command": "zed.getConsoleContent", + "category": "Zed", + "title": "Get Console Content" } ], "menus": { @@ -81,6 +86,10 @@ { "category": "Zed", "command": "zed.quartoVisualMode" + }, + { + "category": "Zed", + "command": "zed.getConsoleContent" } ] } diff --git a/extensions/positron-zed/src/commands.ts b/extensions/positron-zed/src/commands.ts index ded2dfcf297b..2d2f55be40ed 100644 --- a/extensions/positron-zed/src/commands.ts +++ b/extensions/positron-zed/src/commands.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as positron from 'positron'; export async function registerCommands(context: vscode.ExtensionContext) { context.subscriptions.push( @@ -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 }); + }), + ); } From ad6f6b7ba776a12062482de3462ffbe5cafa8668 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Fri, 31 Jul 2026 13:13:56 -0500 Subject: [PATCH 8/9] Validate session in getConsoleContent to prevent history leak $getConsoleContent passed untrusted sessionId straight into getExecutionEntries(), which creates-on-read: an unknown ID would allocate and permanently register a SessionExecutionHistory (with a workbench-lifetime onWillSaveState listener), growing memory and the listener list without bound, and returning [] instead of erroring. Validate the session up front via a new sessionConsoleContent helper (mirroring sessionVariableQueries), throwing "No such session" for unknown IDs so the create-on-read path is never reached and the error surface matches the sibling session-scoped read APIs. Add coverage for the known-session and unknown-session (no-allocation) paths. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../positron/mainThreadLanguageRuntime.ts | 7 ++-- .../common/helpers/sessionConsoleContent.ts | 36 ++++++++++++++++++ .../common/executionHistoryService.vitest.ts | 38 +++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleContent.ts diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index 2ce7f2905438..03b4e76d1707 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -55,7 +55,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, projectExecutionEntriesToConsoleContent } from '../../../services/positronHistory/common/executionHistoryService.js'; +import { IExecutionHistoryService } from '../../../services/positronHistory/common/executionHistoryService.js'; +import { getConsoleContent } from '../../../services/positronHistory/common/helpers/sessionConsoleContent.js'; import { isWebviewPreloadMessage, isWebviewReplayMessage } from '../../../services/positronIPyWidgets/common/webviewPreloadUtils.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { LanguageRuntimeDynState } from 'positron'; @@ -1986,8 +1987,8 @@ export class MainThreadLanguageRuntime } async $getConsoleContent(sessionId: string, numberOfEntries?: number): Promise { - return projectExecutionEntriesToConsoleContent( - this._executionHistoryService.getExecutionEntries(sessionId), numberOfEntries); + return getConsoleContent( + this._executionHistoryService, this._runtimeSessionService, sessionId, numberOfEntries); } /** diff --git a/src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleContent.ts b/src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleContent.ts new file mode 100644 index 000000000000..e1f1db568e3e --- /dev/null +++ b/src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleContent.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2026 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRuntimeSessionService } from '../../../runtimeSession/common/runtimeSessionService.js'; +import { IConsoleContentEntry, IExecutionHistoryService, projectExecutionEntriesToConsoleContent } from '../executionHistoryService.js'; + +/** + * Get the recent console content for a session: the code fragments that have + * run, each paired with its output and any error. + * + * The session is validated first, throwing a descriptive error for an unknown + * session ID. This 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. + * + * @param executionHistoryService The execution history service. + * @param runtimeSessionService The runtime session service, used to validate the session. + * @param sessionId The runtime session to read console content for. + * @param numberOfEntries The number of most recent entries to return. + * @returns The projected console content entries, oldest first. + */ +export function getConsoleContent( + executionHistoryService: IExecutionHistoryService, + runtimeSessionService: IRuntimeSessionService, + sessionId: string, + numberOfEntries?: number): IConsoleContentEntry[] { + if (!runtimeSessionService.getSession(sessionId)) { + throw new Error(`No such session: ${sessionId}`); + } + return projectExecutionEntriesToConsoleContent( + executionHistoryService.getExecutionEntries(sessionId), numberOfEntries); +} diff --git a/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts b/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts index 28d3ad19c267..e72b4ad90543 100644 --- a/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts +++ b/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts @@ -10,6 +10,7 @@ import { ILogService, NullLogService } from '../../../../../platform/log/common/ 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, IExecutionHistoryEntry, projectExecutionEntriesToConsoleContent, DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT } from '../../common/executionHistoryService.js'; +import { getConsoleContent } from '../../common/helpers/sessionConsoleContent.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'; @@ -1060,6 +1061,43 @@ describe('ExecutionHistoryService', () => { // store() signature is (key, value, scope, target) -- match scope/target with expect.any(Number). expect(storeSpy).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`positron\\..*\\.${sessionId}`)), null, expect.any(Number), expect.any(Number)); }); + + describe('getConsoleContent', () => { + /** Fire the messages that record a single completed code execution for a session. */ + function recordExecution(session: TestLanguageRuntimeSession, executionId: string, code: string, output: string): void { + const now = new Date().toISOString(); + session.onDidReceiveRuntimeMessageInputEmitter.fire({ parent_id: executionId, id: `input-${executionId}`, code, when: now }); + session.onDidReceiveRuntimeMessageOutputEmitter.fire({ parent_id: executionId, id: `output-${executionId}`, when: now, data: { 'text/plain': output } }); + session.onDidReceiveRuntimeMessageStateEmitter.fire({ parent_id: executionId, id: `state-${executionId}`, state: 'idle', when: now }); + } + + it('projects the recent console content for a known session', () => { + const session = createSession('console-session-1'); + runtimeSessionService.onWillStartSessionEmitter.fire({ + session, + startMode: RuntimeStartMode.Starting, + hasConsole: true, + activate: false + }); + recordExecution(session, 'exec-1', 'print("Hi")', 'Hi'); + + const content = getConsoleContent(executionHistoryService, runtimeSessionService, 'console-session-1'); + + expect(content.map(({ input, output, error }) => ({ input, output, error }))).toEqual([ + { input: 'print("Hi")', output: 'Hi', error: undefined }, + ]); + }); + + it('throws for an unknown session and does not allocate an execution history', () => { + // getExecutionEntries() creates-on-read, so a bogus session ID must be + // rejected before it reaches the service to avoid a permanent leak. + const getEntriesSpy = vi.spyOn(executionHistoryService, 'getExecutionEntries'); + + expect(() => getConsoleContent(executionHistoryService, runtimeSessionService, 'does-not-exist')) + .toThrow(/No such session: does-not-exist/); + expect(getEntriesSpy).not.toHaveBeenCalled(); + }); + }); }); describe('projectExecutionEntriesToConsoleContent', () => { From 314cb8570181cf16b12651480694c684a78ff4a2 Mon Sep 17 00:00:00 2001 From: Sam Clark Date: Fri, 31 Jul 2026 15:30:41 -0500 Subject: [PATCH 9/9] Rename console content API to console history, add privacy setting Rename getConsoleContent to getConsoleHistory across the API surface, protocol, and helpers, and add a console.historyApiEnabled setting (default true) that gates extension access to console history. Co-Authored-By: Claude Opus 4.8 (1M context) --- extensions/positron-zed/package.json | 6 +-- extensions/positron-zed/src/commands.ts | 10 ++-- src/positron-dts/positron.d.ts | 15 ++++-- .../positron/mainThreadLanguageRuntime.ts | 14 ++--- .../positron/extHost.positron.api.impl.ts | 4 +- .../positron/extHost.positron.protocol.ts | 4 +- .../common/positron/extHostLanguageRuntime.ts | 4 +- .../common/executionHistoryService.ts | 25 ++++++--- .../common/helpers/sessionConsoleContent.ts | 36 ------------- .../common/helpers/sessionConsoleHistory.ts | 49 +++++++++++++++++ .../common/executionHistoryService.vitest.ts | 53 +++++++++++++------ 11 files changed, 138 insertions(+), 82 deletions(-) delete mode 100644 src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleContent.ts create mode 100644 src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleHistory.ts diff --git a/extensions/positron-zed/package.json b/extensions/positron-zed/package.json index d7ebd92c054a..bd8c9ffb33ba 100644 --- a/extensions/positron-zed/package.json +++ b/extensions/positron-zed/package.json @@ -81,9 +81,9 @@ } }, { - "command": "zed.getConsoleContent", + "command": "zed.getConsoleHistory", "category": "Zed", - "title": "Get Console Content" + "title": "Get Console History" } ], "menus": { @@ -94,7 +94,7 @@ }, { "category": "Zed", - "command": "zed.getConsoleContent" + "command": "zed.getConsoleHistory" } ] } diff --git a/extensions/positron-zed/src/commands.ts b/extensions/positron-zed/src/commands.ts index 2d2f55be40ed..385ca4c6af83 100644 --- a/extensions/positron-zed/src/commands.ts +++ b/extensions/positron-zed/src/commands.ts @@ -18,19 +18,19 @@ export async function registerCommands(context: vscode.ExtensionContext) { }), // Demo/test the positron.runtime console-history API without needing Posit - // Assistant: reads the foreground session's recent console content and + // 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.getConsoleContent', async () => { + vscode.commands.registerCommand('zed.getConsoleHistory', 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.'); + 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 Content', + 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.'), }); @@ -40,7 +40,7 @@ export async function registerCommands(context: vscode.ExtensionContext) { const numberOfEntries = input === '' ? undefined : Number(input); const sessionId = session.metadata.sessionId; - const entries = await positron.runtime.getConsoleContent(sessionId, numberOfEntries); + 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. diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index c31b978dfea2..a799c5698946 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -3101,7 +3101,7 @@ declare module 'positron' { * A single console execution: a command that ran in a runtime session, * paired with its output and any error. */ - export interface ConsoleContentEntry { + export interface ConsoleHistoryEntry { /** The code that was executed. */ input: string; /** The textual output produced by the execution. */ @@ -3129,14 +3129,19 @@ declare module 'positron' { * 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 + * 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, or an empty - * array when the session has no console 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 getConsoleContent(sessionId: string, numberOfEntries?: number): Thenable; + export function getConsoleHistory(sessionId: string, numberOfEntries?: number): Thenable; /** * Register a handler for runtime client instances. This handler will be called diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index 748a53a87195..8bd23a43d579 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -10,7 +10,7 @@ import { ExtHostPositronContext, RuntimeInitialState, IActiveRuntimeSessionMetadataDto, - ISerializedConsoleContentEntry + 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'; @@ -23,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'; @@ -56,7 +57,7 @@ import { CodeAttributionSource, IConsoleCodeAttribution } from '../../../service 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 { getConsoleContent } from '../../../services/positronHistory/common/helpers/sessionConsoleContent.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'; @@ -1673,7 +1674,8 @@ export class MainThreadLanguageRuntime @IEditorService private readonly _editorService: IEditorService, @IOpenerService private readonly _openerService: IOpenerService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, - @IExecutionHistoryService private readonly _executionHistoryService: IExecutionHistoryService + @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 @@ -2008,9 +2010,9 @@ export class MainThreadLanguageRuntime return querySessionTables(this._positronVariablesService, sessionId, accessKeys, queryTypes); } - async $getConsoleContent(sessionId: string, numberOfEntries?: number): Promise { - return getConsoleContent( - this._executionHistoryService, this._runtimeSessionService, sessionId, numberOfEntries); + async $getConsoleHistory(sessionId: string, numberOfEntries?: number): Promise { + return getConsoleHistory( + this._executionHistoryService, this._runtimeSessionService, this._configurationService, sessionId, numberOfEntries); } /** diff --git a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts index f0eb17cd8702..786f3b9b22b3 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts @@ -179,8 +179,8 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce Thenable> { return extHostLanguageRuntime.querySessionTables(sessionId, accessKeys, queryTypes); }, - getConsoleContent(sessionId: string, numberOfEntries?: number): Thenable { - return extHostLanguageRuntime.getConsoleContent(sessionId, numberOfEntries); + getConsoleHistory(sessionId: string, numberOfEntries?: number): Thenable { + return extHostLanguageRuntime.getConsoleHistory(sessionId, numberOfEntries); }, registerClientHandler(handler: positron.RuntimeClientHandler): vscode.Disposable { return extHostLanguageRuntime.registerClientHandler(handler); diff --git a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts index b127a48be095..96f7a1ae3e53 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts @@ -105,7 +105,7 @@ export interface MainThreadLanguageRuntimeShape extends IDisposable { $getSessionWorkingDirectory(sessionId?: string): Promise; $getSessionVariables(sessionId: string, accessKeys?: Array>): Promise>>; $querySessionTables(sessionId: string, accessKeys: Array>, queryTypes: Array): Promise>; - $getConsoleContent(sessionId: string, numberOfEntries?: number): Promise; + $getConsoleHistory(sessionId: string, numberOfEntries?: number): Promise; $callMethod(sessionId: string, method: string, args: unknown[]): Thenable; $emitPerfMark(extensionId: string, name: string): void; $emitLanguageRuntimeMessage(sessionId: string, handled: boolean, message: SerializableObjectWithBuffers): void; @@ -370,7 +370,7 @@ export type ISerializedValidateAndExecuteCommandResult = message?: string; }; -export interface ISerializedConsoleContentEntry { +export interface ISerializedConsoleHistoryEntry { input: string; output: string; error?: { name: string; message: string; traceback: string[] }; diff --git a/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts b/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts index f2b75a935083..5f13bb1f0158 100644 --- a/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts +++ b/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts @@ -1823,8 +1823,8 @@ export class ExtHostLanguageRuntime implements extHostProtocol.ExtHostLanguageRu return this._proxy.$querySessionTables(sessionId, accessKeys, queryTypes); } - public getConsoleContent(sessionId: string, numberOfEntries?: number): Promise { - return this._proxy.$getConsoleContent(sessionId, numberOfEntries); + public getConsoleHistory(sessionId: string, numberOfEntries?: number): Promise { + return this._proxy.$getConsoleHistory(sessionId, numberOfEntries); } /** diff --git a/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts b/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts index 5a688b89f29a..63a9bf714b49 100644 --- a/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts +++ b/src/vs/workbench/services/positronHistory/common/executionHistoryService.ts @@ -74,7 +74,7 @@ export interface IExecutionHistoryError { * 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 { +export interface IConsoleHistoryEntry { /** The code that was executed. */ input: string; /** The textual output produced by the execution. */ @@ -86,10 +86,17 @@ export interface IConsoleContentEntry { } /** Default number of recent console entries returned when no count is requested. */ -export const DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT = 5; +export const DEFAULT_CONSOLE_HISTORY_ENTRY_COUNT = 5; /** - * Projects raw execution history entries down to the console content relevant + * 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 @@ -98,9 +105,9 @@ export const DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT = 5; * * @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. + * {@link DEFAULT_CONSOLE_HISTORY_ENTRY_COUNT}; non-positive values fall back to it. */ -export function projectExecutionEntriesToConsoleContent(entries: IExecutionHistoryEntry[], numberOfEntries?: number): IConsoleContentEntry[] { +export function projectExecutionEntriesToConsoleHistory(entries: IExecutionHistoryEntry[], numberOfEntries?: number): IConsoleHistoryEntry[] { const projected = entries .filter(entry => entry.outputType === ExecutionEntryType.Execution && entry.input) .map(entry => ({ @@ -110,7 +117,7 @@ export function projectExecutionEntriesToConsoleContent(entries: IExecutionHisto when: entry.when, })); - const count = numberOfEntries && numberOfEntries > 0 ? numberOfEntries : DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT; + const count = numberOfEntries && numberOfEntries > 0 ? numberOfEntries : DEFAULT_CONSOLE_HISTORY_ENTRY_COUNT; return projected.slice(-count); } @@ -224,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 } } }; diff --git a/src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleContent.ts b/src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleContent.ts deleted file mode 100644 index e1f1db568e3e..000000000000 --- a/src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleContent.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2026 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IRuntimeSessionService } from '../../../runtimeSession/common/runtimeSessionService.js'; -import { IConsoleContentEntry, IExecutionHistoryService, projectExecutionEntriesToConsoleContent } from '../executionHistoryService.js'; - -/** - * Get the recent console content for a session: the code fragments that have - * run, each paired with its output and any error. - * - * The session is validated first, throwing a descriptive error for an unknown - * session ID. This 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. - * - * @param executionHistoryService The execution history service. - * @param runtimeSessionService The runtime session service, used to validate the session. - * @param sessionId The runtime session to read console content for. - * @param numberOfEntries The number of most recent entries to return. - * @returns The projected console content entries, oldest first. - */ -export function getConsoleContent( - executionHistoryService: IExecutionHistoryService, - runtimeSessionService: IRuntimeSessionService, - sessionId: string, - numberOfEntries?: number): IConsoleContentEntry[] { - if (!runtimeSessionService.getSession(sessionId)) { - throw new Error(`No such session: ${sessionId}`); - } - return projectExecutionEntriesToConsoleContent( - executionHistoryService.getExecutionEntries(sessionId), numberOfEntries); -} diff --git a/src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleHistory.ts b/src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleHistory.ts new file mode 100644 index 000000000000..62cdc74bed89 --- /dev/null +++ b/src/vs/workbench/services/positronHistory/common/helpers/sessionConsoleHistory.ts @@ -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(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); +} diff --git a/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts b/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts index e72b4ad90543..57d2b43bc1d0 100644 --- a/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts +++ b/src/vs/workbench/services/positronHistory/test/common/executionHistoryService.vitest.ts @@ -9,8 +9,8 @@ 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, IExecutionHistoryEntry, projectExecutionEntriesToConsoleContent, DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT } from '../../common/executionHistoryService.js'; -import { getConsoleContent } from '../../common/helpers/sessionConsoleContent.js'; +import { IExecutionHistoryService, ExecutionEntryType, IExecutionHistoryEntry, projectExecutionEntriesToConsoleHistory, DEFAULT_CONSOLE_HISTORY_ENTRY_COUNT, CONSOLE_HISTORY_API_ENABLED_KEY } from '../../common/executionHistoryService.js'; +import { getConsoleHistory } from '../../common/helpers/sessionConsoleHistory.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'; @@ -118,6 +118,9 @@ class TestRuntimeSessionService implements IRuntimeSessionService { private readonly _onDidStartUiClient = new Emitter(); readonly onDidStartUiClient = this._onDidStartUiClient.event; + private readonly _onDidChangeDisplayRuntimeState = new Emitter<{ sessionId: string; state: RuntimeState }>(); + readonly onDidChangeDisplayRuntimeState = this._onDidChangeDisplayRuntimeState.event; + foregroundSession: ILanguageRuntimeSession | undefined; foregroundSessionDisplayInfo: IRuntimeSessionDisplayInfo | undefined = undefined; private readonly _onDidChangeForegroundSessionDisplayInfo = new Emitter(); @@ -137,6 +140,10 @@ class TestRuntimeSessionService implements IRuntimeSessionService { return this.sessions.get(sessionId); } + getDisplayRuntimeState(_sessionId: string): RuntimeState | undefined { + throw new Error('Method not implemented.'); + } + // Helper method to fire session start event with proper structure fireWillStartSession(session: ILanguageRuntimeSession, startMode: RuntimeStartMode): void { this._onWillStartSession.fire({ @@ -1062,7 +1069,7 @@ describe('ExecutionHistoryService', () => { expect(storeSpy).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`positron\\..*\\.${sessionId}`)), null, expect.any(Number), expect.any(Number)); }); - describe('getConsoleContent', () => { + describe('getConsoleHistory', () => { /** Fire the messages that record a single completed code execution for a session. */ function recordExecution(session: TestLanguageRuntimeSession, executionId: string, code: string, output: string): void { const now = new Date().toISOString(); @@ -1071,7 +1078,7 @@ describe('ExecutionHistoryService', () => { session.onDidReceiveRuntimeMessageStateEmitter.fire({ parent_id: executionId, id: `state-${executionId}`, state: 'idle', when: now }); } - it('projects the recent console content for a known session', () => { + it('projects the recent console history for a known session', () => { const session = createSession('console-session-1'); runtimeSessionService.onWillStartSessionEmitter.fire({ session, @@ -1081,9 +1088,9 @@ describe('ExecutionHistoryService', () => { }); recordExecution(session, 'exec-1', 'print("Hi")', 'Hi'); - const content = getConsoleContent(executionHistoryService, runtimeSessionService, 'console-session-1'); + const history = getConsoleHistory(executionHistoryService, runtimeSessionService, configurationService, 'console-session-1'); - expect(content.map(({ input, output, error }) => ({ input, output, error }))).toEqual([ + expect(history.map(({ input, output, error }) => ({ input, output, error }))).toEqual([ { input: 'print("Hi")', output: 'Hi', error: undefined }, ]); }); @@ -1093,14 +1100,30 @@ describe('ExecutionHistoryService', () => { // rejected before it reaches the service to avoid a permanent leak. const getEntriesSpy = vi.spyOn(executionHistoryService, 'getExecutionEntries'); - expect(() => getConsoleContent(executionHistoryService, runtimeSessionService, 'does-not-exist')) + expect(() => getConsoleHistory(executionHistoryService, runtimeSessionService, configurationService, 'does-not-exist')) .toThrow(/No such session: does-not-exist/); expect(getEntriesSpy).not.toHaveBeenCalled(); }); + + it('throws when the privacy setting is disabled, without reading any session', () => { + const session = createSession('console-session-2'); + runtimeSessionService.onWillStartSessionEmitter.fire({ + session, + startMode: RuntimeStartMode.Starting, + hasConsole: true, + activate: false + }); + configurationService.setUserConfiguration(CONSOLE_HISTORY_API_ENABLED_KEY, false); + const getEntriesSpy = vi.spyOn(executionHistoryService, 'getExecutionEntries'); + + expect(() => getConsoleHistory(executionHistoryService, runtimeSessionService, configurationService, 'console-session-2')) + .toThrow(new RegExp(`"${CONSOLE_HISTORY_API_ENABLED_KEY}" setting is disabled`)); + expect(getEntriesSpy).not.toHaveBeenCalled(); + }); }); }); -describe('projectExecutionEntriesToConsoleContent', () => { +describe('projectExecutionEntriesToConsoleHistory', () => { /** Build an execution history entry, defaulting to a completed code execution. */ function entry(overrides: Partial>): IExecutionHistoryEntry { return { @@ -1122,7 +1145,7 @@ describe('projectExecutionEntriesToConsoleContent', () => { entry({ input: 'print(2)', output: '[1] 2\n', when: 3 }), ]; - expect(projectExecutionEntriesToConsoleContent(entries)).toEqual([ + expect(projectExecutionEntriesToConsoleHistory(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 }, @@ -1136,24 +1159,24 @@ describe('projectExecutionEntriesToConsoleContent', () => { entry({ input: 'real()', output: 42, when: 3 }), ]; - expect(projectExecutionEntriesToConsoleContent(entries)).toEqual([ + expect(projectExecutionEntriesToConsoleHistory(entries)).toEqual([ { input: 'real()', output: '42', when: 3 }, ]); }); - it('limits to the most recent DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT when no count is given', () => { + it('limits to the most recent DEFAULT_CONSOLE_HISTORY_ENTRY_COUNT when no count is given', () => { const entries = Array.from({ length: 8 }, (_, i) => entry({ input: `cmd${i}`, when: i })); - const result = projectExecutionEntriesToConsoleContent(entries); + const result = projectExecutionEntriesToConsoleHistory(entries); expect(result.map(e => e.input)).toEqual(['cmd3', 'cmd4', 'cmd5', 'cmd6', 'cmd7']); - expect(result).toHaveLength(DEFAULT_CONSOLE_CONTENT_ENTRY_COUNT); + expect(result).toHaveLength(DEFAULT_CONSOLE_HISTORY_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); + expect(projectExecutionEntriesToConsoleHistory(entries, 2).map(e => e.input)).toEqual(['cmd6', 'cmd7']); + expect(projectExecutionEntriesToConsoleHistory(entries, 0)).toHaveLength(DEFAULT_CONSOLE_HISTORY_ENTRY_COUNT); }); });