From ebce70673cc8deb99be75d1960cef5f8421f056d Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:19:43 -0500 Subject: [PATCH 01/13] add chat diagnostics positron assistant command --- extensions/positron-assistant/package.json | 6 + .../positron-assistant/package.nls.json | 1 + .../positron-assistant/src/diagnostics.ts | 348 ++++++++++++++++++ .../positron-assistant/src/extension.ts | 10 + 4 files changed, 365 insertions(+) create mode 100644 extensions/positron-assistant/src/diagnostics.ts diff --git a/extensions/positron-assistant/package.json b/extensions/positron-assistant/package.json index f17a5555c5d..cd0b0dbf4d4 100644 --- a/extensions/positron-assistant/package.json +++ b/extensions/positron-assistant/package.json @@ -198,6 +198,12 @@ "title": "%commands.managePromptFiles.title%", "category": "%commands.category%", "enablement": "config.positron.assistant.enable && isDevelopment" + }, + { + "command": "positron-assistant.collectDiagnostics", + "title": "%commands.collectDiagnostics.title%", + "category": "%commands.category%", + "enablement": "config.positron.assistant.enable" } ], "configuration": [ diff --git a/extensions/positron-assistant/package.nls.json b/extensions/positron-assistant/package.nls.json index f263b2e32e3..d41c177c335 100644 --- a/extensions/positron-assistant/package.nls.json +++ b/extensions/positron-assistant/package.nls.json @@ -10,6 +10,7 @@ "commands.cancelGenerateCommitMessage.title": "Cancel Generate Commit Message", "commands.toggleInlineCompletions.title": "Toggle (Enable/Disable) Completions", "commands.managePromptFiles.title": "Manage Prompt Files", + "commands.collectDiagnostics.title": "Collect Diagnostics", "commands.copilot.signin.title": "Copilot Sign In", "commands.copilot.signout.title": "Copilot Sign Out", "commands.category": "Positron Assistant", diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts new file mode 100644 index 00000000000..50d4bef7cff --- /dev/null +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -0,0 +1,348 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import { getStoredModels, getEnabledProviders } from './config'; + +/** + * Helper function to append text to a document editor. + */ +async function appendText(editor: vscode.TextEditor, text: string): Promise { + await editor.edit(builder => { + const lastLine = editor.document.lineAt(editor.document.lineCount - 1); + builder.insert(lastLine.range.end, text); + }); +} + +/** + * Get all Positron Assistant configuration settings with non-default values. + */ +function getAssistantSettings(): string { + const config = vscode.workspace.getConfiguration('positron.assistant'); + const settings: Record = {}; + + // List of all Positron Assistant settings + const settingKeys = [ + 'enable', + 'toolDetails.enable', + 'useAnthropicSdk', + 'streamingEdits.enable', + 'inlineCompletions.enable', + 'inlineCompletionExcludes', + 'gitIntegration.enable', + 'showTokenUsage.enable', + 'maxInputTokens', + 'maxOutputTokens', + 'followups.enable', + 'consoleActions.enable', + 'notebookMode.enable', + 'toolErrors.propagate', + 'alwaysIncludeCopilotTools', + 'providerTimeout', + 'maxConnectionAttempts', + 'filterModels', + 'preferredModel', + 'defaultModels', + 'providerVariables.bedrock', + 'enabledProviders', + ]; + + for (const key of settingKeys) { + const inspection = config.inspect(key); + const value = config.get(key); + + // Only include settings that differ from default + if (inspection && value !== inspection.defaultValue) { + // Check if it's not just an empty array/object matching default empty array/object + const isEmptyArrayOrObject = + (Array.isArray(value) && Array.isArray(inspection.defaultValue) && + value.length === 0 && inspection.defaultValue.length === 0) || + (typeof value === 'object' && typeof inspection.defaultValue === 'object' && + !Array.isArray(value) && !Array.isArray(inspection.defaultValue) && + Object.keys(value).length === 0 && Object.keys(inspection.defaultValue).length === 0); + + if (!isEmptyArrayOrObject) { + settings[`positron.assistant.${key}`] = value; + } + } + } + + if (Object.keys(settings).length === 0) { + return '\n // No non-default settings configured'; + } + + return '\n' + Object.entries(settings) + .map(([key, value]) => ` "${key}": ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`) + .join(',\n'); +} + +/** + * Get GitHub Copilot Chat configuration settings with non-default values. + */ +function getCopilotChatSettings(): string { + const config = vscode.workspace.getConfiguration('github.copilot'); + const settings: Record = {}; + + // Key Copilot settings that may affect Assistant behavior + const settingKeys = [ + 'enable', + 'chat.enableChatCompletion', + 'advanced.debug.useElectronFetcher', + 'advanced.debug.useNodeFetcher', + 'advanced.debug.useNodeFetchFetcher', + ]; + + for (const key of settingKeys) { + const inspection = config.inspect(key); + const value = config.get(key); + + if (inspection && value !== inspection.defaultValue) { + settings[`github.copilot.${key}`] = value; + } + } + + if (Object.keys(settings).length === 0) { + return '\n // No non-default Copilot settings configured'; + } + + return '\n' + Object.entries(settings) + .map(([key, value]) => ` "${key}": ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`) + .join(',\n'); +} + +/** + * Get information about stored language models and providers. + */ +function getModelInfo(context: vscode.ExtensionContext): string { + const storedModels = getStoredModels(context); + + if (storedModels.length === 0) { + return 'No models configured'; + } + + return storedModels.map(model => { + // Sanitize sensitive information + const sanitized = { + id: model.id, + provider: model.provider, + name: model.name, + model: model.model, + type: model.type, + toolCalls: model.toolCalls, + completions: model.completions, + baseUrl: model.baseUrl ? '[REDACTED]' : undefined, + maxInputTokens: model.maxInputTokens, + maxOutputTokens: model.maxOutputTokens, + }; + + return `- **${model.name}** + - Provider: ${model.provider} + - Type: ${model.type} + - Model ID: ${model.model} + - Tool Calls: ${model.toolCalls ?? 'N/A'} + - Completions: ${model.completions ?? 'N/A'} + - Base URL: ${sanitized.baseUrl ?? 'default'} + - Max Input Tokens: ${model.maxInputTokens ?? 'default'} + - Max Output Tokens: ${model.maxOutputTokens ?? 'default'}`; + }).join('\n\n'); +} + +/** + * Get available language model providers from VS Code's Language Model API. + */ +async function getAvailableProviders(): Promise { + try { + const models = await vscode.lm.selectChatModels(); + + if (models.length === 0) { + return 'No language models available through VS Code API'; + } + + // Group by vendor + const byVendor: Record = {}; + for (const model of models) { + if (!byVendor[model.vendor]) { + byVendor[model.vendor] = []; + } + byVendor[model.vendor].push(model); + } + + const sections = Object.entries(byVendor).map(([vendor, vendorModels]) => { + const modelList = vendorModels + .map(m => ` - ${m.name} (${m.id}) - Max Input: ${m.maxInputTokens ?? 'unknown'}`) + .join('\n'); + return `**${vendor}** (${vendorModels.length} model${vendorModels.length !== 1 ? 's' : ''})\n${modelList}`; + }); + + return sections.join('\n\n'); + } catch (error) { + return `Error retrieving models: ${error instanceof Error ? error.message : String(error)}`; + } +} + +/** + * Get enabled providers from Positron AI API. + */ +async function getPositronProviders(): Promise { + try { + const providers = await positron.ai.getSupportedProviders(); + if (providers.length === 0) { + return 'No supported providers from Positron AI'; + } + return providers.map(p => `- ${p}`).join('\n'); + } catch (error) { + return `Error retrieving Positron providers: ${error instanceof Error ? error.message : String(error)}`; + } +} + +/** + * Get chat export data. + */ +async function getChatExportInfo(): Promise { + try { + const chatExport = await positron.ai.getChatExport(); + if (!chatExport) { + return 'No active chat session'; + } + + // Cast to access internal structure (API returns object type for stability) + const chatData = chatExport as any; + + if (!chatData.requests || !Array.isArray(chatData.requests)) { + return 'Chat session found but data format is unexpected'; + } + + const requestCount = chatData.requests.length; + + // Get the model/provider from the most recent request + let currentModel = 'Unknown'; + if (requestCount > 0) { + const lastRequest = chatData.requests[requestCount - 1]; + if (lastRequest.response?.agent) { + currentModel = lastRequest.response.agent; + } + } + + return `Active chat session found: +- Total requests: ${requestCount} +- Current agent/model: ${currentModel} +- Location: ${chatData.initialLocation || 'N/A'}`; + } catch (error) { + return `Error retrieving chat export: ${error instanceof Error ? error.message : String(error)}`; + } +} + +/** + * Get extension information. + */ +function getExtensionInfo(): string { + const assistantExt = vscode.extensions.getExtension('positron.positron-assistant'); + const copilotExt = vscode.extensions.getExtension('github.copilot-chat'); + + const assistantInfo = assistantExt + ? `Version ${assistantExt.packageJSON.version}${assistantExt.isActive ? ' (Active)' : ' (Inactive)'}` + : 'Not installed'; + + const copilotInfo = copilotExt + ? `Version ${copilotExt.packageJSON.version}${copilotExt.isActive ? ' (Active)' : ' (Inactive)'}` + : 'Not installed'; + + return `- Positron Assistant: ${assistantInfo} +- GitHub Copilot Chat: ${copilotInfo} +- VS Code: ${vscode.version} +- Positron: ${vscode.env.appName} +- OS: ${process.platform} ${process.arch}${vscode.env.remoteName ? `\n- Remote: ${vscode.env.remoteName}` : ''}`; +} + +/** + * Get log output from the Assistant output channel. + * Note: VS Code doesn't provide API access to output channel content, + * so we can only provide a reference. + */ +function getLogReference(): string { + return `Log output is available in the Output panel: +- View → Output +- Select "Assistant" from the dropdown +- Select "GitHub Copilot Language Server" for Copilot logs + +To include logs in a bug report: +1. Reproduce the issue +2. Open the Output panel +3. Copy relevant log entries manually`; +} + +/** + * Collect and display comprehensive diagnostics for Positron Assistant. + */ +export async function collectDiagnostics(context: vscode.ExtensionContext): Promise { + // Create a new untitled markdown document + const document = await vscode.workspace.openTextDocument({ + language: 'markdown', + content: '' + }); + const editor = await vscode.window.showTextDocument(document); + + // Header + await appendText(editor, '# Positron Assistant Diagnostics\n\n'); + await appendText(editor, `Generated: ${new Date().toISOString()}\n\n`); + + // Extension Information + await appendText(editor, '## Extension Information\n\n'); + await appendText(editor, getExtensionInfo()); + await appendText(editor, '\n\n'); + + // Configuration Settings + await appendText(editor, '## Configuration Settings\n\n'); + await appendText(editor, '### Positron Assistant Settings (Non-Default)\n\n'); + await appendText(editor, '```json' + getAssistantSettings() + '\n```\n\n'); + + await appendText(editor, '### GitHub Copilot Settings (Non-Default)\n\n'); + await appendText(editor, '```json' + getCopilotChatSettings() + '\n```\n\n'); + + // Providers + await appendText(editor, '## Language Model Providers\n\n'); + + await appendText(editor, '### Enabled Providers (Configuration)\n\n'); + try { + const enabledProviders = await getEnabledProviders(); + if (enabledProviders.length === 0) { + await appendText(editor, 'All providers enabled (no filter configured)\n\n'); + } else { + await appendText(editor, enabledProviders.map(p => `- ${p}`).join('\n') + '\n\n'); + } + } catch (error) { + await appendText(editor, `Error: ${error instanceof Error ? error.message : String(error)}\n\n`); + } + + await appendText(editor, '### Positron Supported Providers\n\n'); + await appendText(editor, await getPositronProviders()); + await appendText(editor, '\n\n'); + + await appendText(editor, '### Available Models (VS Code Language Model API)\n\n'); + await appendText(editor, await getAvailableProviders()); + await appendText(editor, '\n\n'); + + // Configured Models + await appendText(editor, '## Configured Models\n\n'); + await appendText(editor, getModelInfo(context)); + await appendText(editor, '\n\n'); + + // Active Chat Session + await appendText(editor, '## Active Chat Session\n\n'); + await appendText(editor, await getChatExportInfo()); + await appendText(editor, '\n\n'); + + // Logs + await appendText(editor, '## Logs\n\n'); + await appendText(editor, getLogReference()); + await appendText(editor, '\n\n'); + + // Footer + await appendText(editor, '---\n\n'); + await appendText(editor, '## Documentation\n\n'); + await appendText(editor, '- [Positron Assistant Documentation](https://positron.posit.co/assistant)\n'); + await appendText(editor, '- [Report Issues](https://github.com/posit-dev/positron/issues)\n'); +} diff --git a/extensions/positron-assistant/src/extension.ts b/extensions/positron-assistant/src/extension.ts index 8dba97e700d..d3819281c44 100644 --- a/extensions/positron-assistant/src/extension.ts +++ b/extensions/positron-assistant/src/extension.ts @@ -24,6 +24,7 @@ import { registerParticipantDetectionProvider } from './participantDetection.js' import { registerAssistantCommands } from './commands/index.js'; import { PositronAssistantApi } from './api.js'; import { registerPromptManagement } from './promptRender.js'; +import { collectDiagnostics } from './diagnostics.js'; const hasChatModelsContextKey = 'positron-assistant.hasChatModels'; @@ -276,6 +277,14 @@ function registerToggleInlineCompletionsCommand(context: vscode.ExtensionContext ); } +function registerCollectDiagnosticsCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('positron-assistant.collectDiagnostics', async () => { + await collectDiagnostics(context); + }) + ); +} + async function toggleInlineCompletions() { // Get the current value of the setting const config = vscode.workspace.getConfiguration('positron.assistant'); @@ -336,6 +345,7 @@ function registerAssistant(context: vscode.ExtensionContext) { registerGenerateNotebookSuggestionsCommand(context, participantService, log); registerExportChatCommands(context); registerToggleInlineCompletionsCommand(context); + registerCollectDiagnosticsCommand(context); registerPromptManagement(context); // Register mapped edits provider From 8efb9572c88fff09096dfa2cd3d55b834e7f8034 Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:32:57 -0500 Subject: [PATCH 02/13] include Assistant logs in diagnostics --- .../positron-assistant/src/diagnostics.ts | 46 +++-- .../positron-assistant/src/extension.ts | 5 +- .../positron-assistant/src/logBuffer.ts | 186 ++++++++++++++++++ 3 files changed, 223 insertions(+), 14 deletions(-) create mode 100644 extensions/positron-assistant/src/logBuffer.ts diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index 50d4bef7cff..698cbd1a6ab 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import * as positron from 'positron'; import { getStoredModels, getEnabledProviders } from './config'; +import { log } from './extension'; /** * Helper function to append text to a document editor. @@ -259,18 +260,28 @@ function getExtensionInfo(): string { /** * Get log output from the Assistant output channel. - * Note: VS Code doesn't provide API access to output channel content, - * so we can only provide a reference. */ -function getLogReference(): string { - return `Log output is available in the Output panel: -- View → Output -- Select "Assistant" from the dropdown -- Select "GitHub Copilot Language Server" for Copilot logs - -To include logs in a bug report: -1. Reproduce the issue -2. Open the Output panel +function getAssistantLogs(includeTraceLevel: boolean = false): string { + const level = includeTraceLevel ? 'trace' : 'debug'; + const logs = log.formatEntriesForDiagnostics(500, level); + + if (logs === 'No log entries available') { + return 'No log entries captured yet. Logs are captured from the moment the extension loads.'; + } + + return logs; +} + +/** + * Get Copilot Language Server log reference. + * Note: We can't access Copilot's logs directly as they're in a separate extension. + */ +function getCopilotLogReference(): string { + return `Copilot Language Server logs are managed separately and cannot be captured automatically. + +To include Copilot logs in a bug report: +1. Open the Output panel (View → Output) +2. Select "GitHub Copilot Language Server" from the dropdown 3. Copy relevant log entries manually`; } @@ -336,8 +347,17 @@ export async function collectDiagnostics(context: vscode.ExtensionContext): Prom await appendText(editor, '\n\n'); // Logs - await appendText(editor, '## Logs\n\n'); - await appendText(editor, getLogReference()); + await appendText(editor, '## Positron Assistant Logs\n\n'); + await appendText(editor, 'Recent log entries (last 500, debug level and above):\n\n'); + await appendText(editor, '> **Note**: To include trace-level logs, set the log level to "Trace" in:\n'); + await appendText(editor, '> Settings → Extensions → Positron Assistant → Log Level, or\n'); + await appendText(editor, '> Output panel → Assistant → Set Log Level (gear icon)\n\n'); + await appendText(editor, '```\n'); + await appendText(editor, getAssistantLogs()); + await appendText(editor, '\n```\n\n'); + + await appendText(editor, '## GitHub Copilot Logs\n\n'); + await appendText(editor, getCopilotLogReference()); await appendText(editor, '\n\n'); // Footer diff --git a/extensions/positron-assistant/src/extension.ts b/extensions/positron-assistant/src/extension.ts index d3819281c44..43da95ee6b6 100644 --- a/extensions/positron-assistant/src/extension.ts +++ b/extensions/positron-assistant/src/extension.ts @@ -25,6 +25,7 @@ import { registerAssistantCommands } from './commands/index.js'; import { PositronAssistantApi } from './api.js'; import { registerPromptManagement } from './promptRender.js'; import { collectDiagnostics } from './diagnostics.js'; +import { BufferedLogOutputChannel } from './logBuffer.js'; const hasChatModelsContextKey = 'positron-assistant.hasChatModels'; @@ -68,7 +69,9 @@ export function disposeModels(id?: string) { } } -export const log = vscode.window.createOutputChannel('Assistant', { log: true }); +export const log = new BufferedLogOutputChannel( + vscode.window.createOutputChannel('Assistant', { log: true }) +); export async function registerModel(config: StoredModelConfig, context: vscode.ExtensionContext, storage: SecretStorage) { try { diff --git a/extensions/positron-assistant/src/logBuffer.ts b/extensions/positron-assistant/src/logBuffer.ts new file mode 100644 index 00000000000..f2308ba0403 --- /dev/null +++ b/extensions/positron-assistant/src/logBuffer.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export interface LogEntry { + timestamp: Date; + level: 'trace' | 'debug' | 'info' | 'warn' | 'error'; + message: string; +} + +/** + * A wrapper around LogOutputChannel that maintains an in-memory circular buffer + * of recent log entries for diagnostics collection. + */ +export class BufferedLogOutputChannel implements vscode.LogOutputChannel { + private readonly buffer: LogEntry[] = []; + private readonly maxEntries: number; + + constructor( + private readonly channel: vscode.LogOutputChannel, + maxEntries: number = 1000 + ) { + this.maxEntries = maxEntries; + } + + // Implement LogOutputChannel interface by delegating to the wrapped channel + get logLevel(): vscode.LogLevel { + return this.channel.logLevel; + } + + get onDidChangeLogLevel(): vscode.Event { + return this.channel.onDidChangeLogLevel; + } + + get name(): string { + return this.channel.name; + } + + append(value: string): void { + this.channel.append(value); + } + + appendLine(value: string): void { + this.channel.appendLine(value); + } + + replace(value: string): void { + this.channel.replace(value); + } + + clear(): void { + this.channel.clear(); + } + + show(preserveFocus?: boolean): void; + show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; + show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { + if (typeof columnOrPreserveFocus === 'boolean') { + this.channel.show(columnOrPreserveFocus); + } else { + this.channel.show(columnOrPreserveFocus, preserveFocus); + } + } + + hide(): void { + this.channel.hide(); + } + + dispose(): void { + this.channel.dispose(); + } + + private addToBuffer(level: LogEntry['level'], message: string): void { + this.buffer.push({ + timestamp: new Date(), + level, + message + }); + + // Keep buffer size under limit (circular buffer behavior) + if (this.buffer.length > this.maxEntries) { + this.buffer.shift(); + } + } + + trace(message: string, ...args: any[]): void { + const formattedMessage = this.formatMessage(message, args); + this.addToBuffer('trace', formattedMessage); + this.channel.trace(message, ...args); + } + + debug(message: string, ...args: any[]): void { + const formattedMessage = this.formatMessage(message, args); + this.addToBuffer('debug', formattedMessage); + this.channel.debug(message, ...args); + } + + info(message: string, ...args: any[]): void { + const formattedMessage = this.formatMessage(message, args); + this.addToBuffer('info', formattedMessage); + this.channel.info(message, ...args); + } + + warn(message: string, ...args: any[]): void { + const formattedMessage = this.formatMessage(message, args); + this.addToBuffer('warn', formattedMessage); + this.channel.warn(message, ...args); + } + + error(message: string | Error, ...args: any[]): void { + const formattedMessage = message instanceof Error + ? `${message.message}\n${message.stack}` + : this.formatMessage(message, args); + this.addToBuffer('error', formattedMessage); + this.channel.error(message, ...args); + } + + /** + * Get recent log entries from the buffer. + * @param count Number of entries to retrieve (default: all) + * @param level Minimum log level to include (default: all) + */ + getRecentEntries(count?: number, level?: LogEntry['level']): LogEntry[] { + let entries = [...this.buffer]; + + // Filter by level if specified + if (level) { + const levels: LogEntry['level'][] = ['trace', 'debug', 'info', 'warn', 'error']; + const minLevelIndex = levels.indexOf(level); + entries = entries.filter(entry => levels.indexOf(entry.level) >= minLevelIndex); + } + + // Limit count if specified + if (count !== undefined && count < entries.length) { + entries = entries.slice(-count); + } + + return entries; + } + + /** + * Format log entries as text for inclusion in diagnostics. + * @param count Number of entries to include (default: 500) + * @param level Minimum log level to include (default: 'debug') + */ + formatEntriesForDiagnostics(count: number = 500, level: LogEntry['level'] = 'debug'): string { + const entries = this.getRecentEntries(count, level); + + if (entries.length === 0) { + return 'No log entries available'; + } + + const formatted = entries.map(entry => { + const timestamp = entry.timestamp.toISOString(); + const levelStr = entry.level.toUpperCase().padEnd(5); + return `[${timestamp}] ${levelStr} ${entry.message}`; + }).join('\n'); + + const totalInBuffer = this.buffer.length; + const note = entries.length < totalInBuffer + ? `\n\n(Showing ${entries.length} of ${totalInBuffer} total entries in buffer)` + : ''; + + return formatted + note; + } + + /** + * Clear the log buffer. + */ + clearBuffer(): void { + this.buffer.length = 0; + } + + private formatMessage(message: string, args: any[]): string { + if (args.length === 0) { + return message; + } + // Simple formatting - VS Code handles the actual formatting for the output channel + return message + (args.length > 0 ? ' ' + args.map(a => + typeof a === 'object' ? JSON.stringify(a) : String(a) + ).join(' ') : ''); + } +} From 4accdeb333952615b562ced956aa31e5a705e0f5 Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:33:54 -0500 Subject: [PATCH 03/13] add copilot logs --- .../positron-assistant/src/diagnostics.ts | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index 698cbd1a6ab..e79b926f367 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -272,6 +272,41 @@ function getAssistantLogs(includeTraceLevel: boolean = false): string { return logs; } +/** + * Get Copilot Language Server logs. + * Attempts to access the buffered log target from the Copilot Chat extension. + */ +function getCopilotLogs(): string { + try { + const copilotExt = vscode.extensions.getExtension('github.copilot-chat'); + if (!copilotExt || !copilotExt.isActive) { + return 'GitHub Copilot Chat extension is not active'; + } + + // Access the log target getter function from the extension exports + const getCopilotLogTarget = (copilotExt.exports as any)?.getCopilotLogTarget; + if (typeof getCopilotLogTarget !== 'function') { + return 'Unable to access Copilot Chat logs (buffered logging may not be enabled)'; + } + + const logTarget = getCopilotLogTarget(); + if (!logTarget || typeof logTarget.formatEntriesForDiagnostics !== 'function') { + return 'Unable to access Copilot Chat logs (log target not initialized)'; + } + + // LogLevel.Debug = 2 in Copilot Chat's LogLevel enum (Off=0, Trace=1, Debug=2, Info=3, Warning=4, Error=5) + const logs = logTarget.formatEntriesForDiagnostics(500, 2); + + if (logs === 'No log entries available') { + return 'No Copilot Chat log entries captured yet'; + } + + return logs; + } catch (error) { + return `Error retrieving Copilot Chat logs: ${error instanceof Error ? error.message : String(error)}`; + } +} + /** * Get Copilot Language Server log reference. * Note: We can't access Copilot's logs directly as they're in a separate extension. @@ -356,8 +391,17 @@ export async function collectDiagnostics(context: vscode.ExtensionContext): Prom await appendText(editor, getAssistantLogs()); await appendText(editor, '\n```\n\n'); - await appendText(editor, '## GitHub Copilot Logs\n\n'); - await appendText(editor, getCopilotLogReference()); + await appendText(editor, '## GitHub Copilot Chat Logs\n\n'); + await appendText(editor, 'Recent log entries (last 500, debug level and above):\n\n'); + const copilotLogs = getCopilotLogs(); + if (copilotLogs.includes('extension is not active') || copilotLogs.includes('Unable to access')) { + await appendText(editor, `> ${copilotLogs}\n\n`); + await appendText(editor, getCopilotLogReference()); + } else { + await appendText(editor, '```\n'); + await appendText(editor, copilotLogs); + await appendText(editor, '\n```\n'); + } await appendText(editor, '\n\n'); // Footer From 8ae0655bc61f4bd503b3a7c79fbffb6702e0431e Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:08:27 -0500 Subject: [PATCH 04/13] add reset assistant state command --- extensions/positron-assistant/package.json | 6 + .../positron-assistant/package.nls.json | 1 + .../positron-assistant/src/diagnostics.ts | 5 +- .../positron-assistant/src/extension.ts | 10 + extensions/positron-assistant/src/reset.ts | 247 ++++++++++++++++++ 5 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 extensions/positron-assistant/src/reset.ts diff --git a/extensions/positron-assistant/package.json b/extensions/positron-assistant/package.json index cd0b0dbf4d4..bb02931d3ee 100644 --- a/extensions/positron-assistant/package.json +++ b/extensions/positron-assistant/package.json @@ -204,6 +204,12 @@ "title": "%commands.collectDiagnostics.title%", "category": "%commands.category%", "enablement": "config.positron.assistant.enable" + }, + { + "command": "positron-assistant.resetState", + "title": "%commands.resetState.title%", + "category": "%commands.category%", + "enablement": "config.positron.assistant.enable" } ], "configuration": [ diff --git a/extensions/positron-assistant/package.nls.json b/extensions/positron-assistant/package.nls.json index d41c177c335..ff8bc6550b5 100644 --- a/extensions/positron-assistant/package.nls.json +++ b/extensions/positron-assistant/package.nls.json @@ -11,6 +11,7 @@ "commands.toggleInlineCompletions.title": "Toggle (Enable/Disable) Completions", "commands.managePromptFiles.title": "Manage Prompt Files", "commands.collectDiagnostics.title": "Collect Diagnostics", + "commands.resetState.title": "Reset Assistant State", "commands.copilot.signin.title": "Copilot Sign In", "commands.copilot.signout.title": "Copilot Sign Out", "commands.category": "Positron Assistant", diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index e79b926f367..5fcf49d954f 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -253,8 +253,9 @@ function getExtensionInfo(): string { return `- Positron Assistant: ${assistantInfo} - GitHub Copilot Chat: ${copilotInfo} -- VS Code: ${vscode.version} -- Positron: ${vscode.env.appName} +- Positron: ${positron.version} (build ${positron.buildNumber}) +- Code OSS: ${vscode.version} +- Application: ${vscode.env.appName} - OS: ${process.platform} ${process.arch}${vscode.env.remoteName ? `\n- Remote: ${vscode.env.remoteName}` : ''}`; } diff --git a/extensions/positron-assistant/src/extension.ts b/extensions/positron-assistant/src/extension.ts index 43da95ee6b6..92273dd6560 100644 --- a/extensions/positron-assistant/src/extension.ts +++ b/extensions/positron-assistant/src/extension.ts @@ -26,6 +26,7 @@ import { PositronAssistantApi } from './api.js'; import { registerPromptManagement } from './promptRender.js'; import { collectDiagnostics } from './diagnostics.js'; import { BufferedLogOutputChannel } from './logBuffer.js'; +import { resetAssistantState } from './reset.js'; const hasChatModelsContextKey = 'positron-assistant.hasChatModels'; @@ -288,6 +289,14 @@ function registerCollectDiagnosticsCommand(context: vscode.ExtensionContext) { ); } +function registerResetCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('positron-assistant.resetState', async () => { + await resetAssistantState(context); + }) + ); +} + async function toggleInlineCompletions() { // Get the current value of the setting const config = vscode.workspace.getConfiguration('positron.assistant'); @@ -349,6 +358,7 @@ function registerAssistant(context: vscode.ExtensionContext) { registerExportChatCommands(context); registerToggleInlineCompletionsCommand(context); registerCollectDiagnosticsCommand(context); + registerResetCommand(context); registerPromptManagement(context); // Register mapped edits provider diff --git a/extensions/positron-assistant/src/reset.ts b/extensions/positron-assistant/src/reset.ts new file mode 100644 index 00000000000..0728d5f6531 --- /dev/null +++ b/extensions/positron-assistant/src/reset.ts @@ -0,0 +1,247 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as path from 'path'; +import * as fs from 'fs'; +import { collectDiagnostics } from './diagnostics'; +import { CopilotService } from './copilot'; +import { PositLanguageModel } from './posit'; +import { getStoredModels, GlobalSecretStorage } from './config'; +import { disposeModels } from './extension'; + +/** + * Save diagnostics to a file in a reasonable location. + * Priority: workspace folder > home directory > temp directory + */ +async function saveDiagnosticsToFile(context: vscode.ExtensionContext): Promise { + try { + // First, collect the diagnostics (this opens a new untitled document) + await collectDiagnostics(context); + + // Wait a moment for the diagnostics to finish collecting + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Get the active editor which should be the diagnostics document + const editor = vscode.window.activeTextEditor; + if (!editor) { + throw new Error('No active editor found after collecting diagnostics'); + } + + const content = editor.document.getText(); + + // Generate timestamp for filename + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T').join('_').substring(0, 19); + const filename = `positron-assistant-diagnostics-${timestamp}.md`; + + // Determine save location + let saveDir: string | undefined; + + // Try workspace folder first + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + saveDir = workspaceFolders[0].uri.fsPath; + } + + // Fall back to home directory + if (!saveDir) { + const homeDir = process.env.HOME || process.env.USERPROFILE; + if (homeDir) { + saveDir = homeDir; + } + } + + // Last resort: temp directory + if (!saveDir) { + saveDir = process.env.TMPDIR || process.env.TEMP || '/tmp'; + } + + const filePath = path.join(saveDir, filename); + + // Save to file + fs.writeFileSync(filePath, content, 'utf8'); + + // Close the diagnostics document without prompting to save + // We need to revert the document first to mark it as not dirty + await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); + + return filePath; + } catch (error) { + vscode.window.showErrorMessage( + vscode.l10n.t('Failed to save diagnostics: {0}', error instanceof Error ? error.message : String(error)) + ); + return undefined; + } +} + +/** + * Sign out of all providers and clear their authentication state. + */ +async function signOutAllProviders(context: vscode.ExtensionContext): Promise { + const storage = new GlobalSecretStorage(context); + const storedModels = getStoredModels(context); + + // Track unique providers to sign out + const providers = new Set(); + storedModels.forEach(model => providers.add(model.provider)); + + // Sign out of each provider + for (const provider of providers) { + try { + if (provider === 'copilot') { + await CopilotService.instance().signOut(); + } else if (provider === 'posit') { + await PositLanguageModel.signOut(storage); + } + // Other providers that use OAuth or API keys will be cleared via secret storage + } catch (error) { + vscode.window.showWarningMessage( + vscode.l10n.t('Failed to sign out of {0}: {1}', provider, error instanceof Error ? error.message : String(error)) + ); + } + } +} + +/** + * Clear all Assistant state from global state and secret storage. + */ +async function clearAssistantState(context: vscode.ExtensionContext): Promise { + // Dispose all registered models + disposeModels(); + + // Clear global state + const globalStateKeys = context.globalState.keys(); + for (const key of globalStateKeys) { + if (key.startsWith('positron.assistant') || key.includes('assistant')) { + await context.globalState.update(key, undefined); + } + } + + // Clear secret storage - we need to clear known secret keys + const storage = new GlobalSecretStorage(context); + const storedModels = getStoredModels(context); + + // Clear API keys for all stored models + for (const model of storedModels) { + try { + await storage.delete(`apiKey-${model.id}`); + } catch { + // Ignore errors - key might not exist + } + } + + // Clear the models list itself + await context.globalState.update('positron.assistant.models', undefined); + + // Clear other known secret keys + const knownSecrets = [ + 'posit.token', + 'posit.refreshToken', + 'copilot.token' + ]; + + for (const secret of knownSecrets) { + try { + await storage.delete(secret); + } catch { + // Ignore errors + } + } +} + +/** + * Clear all chat history and sessions. + */ +async function clearChatHistory(): Promise { + try { + // Use VS Code's built-in command to clear all chat history + await vscode.commands.executeCommand('workbench.action.chat.clearHistory'); + + // Also clear the chat widget input history + await vscode.commands.executeCommand('workbench.action.chat.clearInputHistory'); + } catch (error) { + vscode.window.showWarningMessage( + vscode.l10n.t('Failed to clear chat history: {0}', error instanceof Error ? error.message : String(error)) + ); + } +} + +/** + * Reset all Positron Assistant state. + * + * This command: + * 1. Generates and saves diagnostic information + * 2. Signs out of all providers (including Copilot) + * 3. Clears all Assistant state (global state and secrets) + * 4. Deletes all chat history + * 5. Reloads the window for a clean start + */ +export async function resetAssistantState(context: vscode.ExtensionContext): Promise { + // Show confirmation dialog + const result = await vscode.window.showWarningMessage( + vscode.l10n.t('Reset Positron Assistant State'), + { + modal: true, + detail: vscode.l10n.t( + 'This will:\n' + + '• Generate and save diagnostic information\n' + + '• Sign out of all language model providers (including Copilot)\n' + + '• Clear all Assistant configuration and state\n' + + '• Delete all chat history\n' + + '• Reload the window\n\n' + + 'This action cannot be undone.' + ) + }, + vscode.l10n.t('Reset'), + vscode.l10n.t('Cancel') + ); + + if (result !== vscode.l10n.t('Reset')) { + return; + } + + // Show progress + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Resetting Positron Assistant'), + cancellable: false + }, + async (progress) => { + // Step 1: Save diagnostics + progress.report({ increment: 0, message: vscode.l10n.t('Saving diagnostics...') }); + const diagnosticsPath = await saveDiagnosticsToFile(context); + if (diagnosticsPath) { + vscode.window.showInformationMessage( + vscode.l10n.t('Diagnostics saved to: {0}', diagnosticsPath) + ); + } + + // Step 2: Sign out of all providers + progress.report({ increment: 20, message: vscode.l10n.t('Signing out of providers...') }); + await signOutAllProviders(context); + + // Step 3: Clear Assistant state + progress.report({ increment: 40, message: vscode.l10n.t('Clearing Assistant state...') }); + await clearAssistantState(context); + + // Step 4: Clear chat history + progress.report({ increment: 60, message: vscode.l10n.t('Clearing chat history...') }); + await clearChatHistory(); + + // Step 5: Reload window + progress.report({ increment: 80, message: vscode.l10n.t('Reloading window...') }); + await new Promise(resolve => setTimeout(resolve, 500)); // Brief pause + + vscode.window.showInformationMessage( + vscode.l10n.t('Assistant state has been reset. The window will now reload.') + ); + + // Reload the window + await vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + ); +} From 4837b0f25b00985e1069c45edc5fd02d0264028e Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:39:26 -0500 Subject: [PATCH 05/13] output cleanup --- .../positron-assistant/src/diagnostics.ts | 107 +++++++++--------- 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index 5fcf49d954f..808a8fec999 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import * as positron from 'positron'; import { getStoredModels, getEnabledProviders } from './config'; import { log } from './extension'; +import { DEFAULT_MAX_TOKEN_INPUT, DEFAULT_MAX_TOKEN_OUTPUT } from './constants.js'; /** * Helper function to append text to a document editor. @@ -25,7 +26,8 @@ function getAssistantSettings(): string { const config = vscode.workspace.getConfiguration('positron.assistant'); const settings: Record = {}; - // List of all Positron Assistant settings + + // VS Code's API doesn't provide a way to iterate over keys so we maintain a list here const settingKeys = [ 'enable', 'toolDetails.enable', @@ -117,38 +119,52 @@ function getCopilotChatSettings(): string { /** * Get information about stored language models and providers. */ -function getModelInfo(context: vscode.ExtensionContext): string { +async function getModelInfo(context: vscode.ExtensionContext): Promise { const storedModels = getStoredModels(context); if (storedModels.length === 0) { return 'No models configured'; } - return storedModels.map(model => { - // Sanitize sensitive information - const sanitized = { - id: model.id, - provider: model.provider, - name: model.name, - model: model.model, - type: model.type, - toolCalls: model.toolCalls, - completions: model.completions, - baseUrl: model.baseUrl ? '[REDACTED]' : undefined, - maxInputTokens: model.maxInputTokens, - maxOutputTokens: model.maxOutputTokens, - }; - - return `- **${model.name}** - - Provider: ${model.provider} - - Type: ${model.type} - - Model ID: ${model.model} - - Tool Calls: ${model.toolCalls ?? 'N/A'} - - Completions: ${model.completions ?? 'N/A'} - - Base URL: ${sanitized.baseUrl ?? 'default'} - - Max Input Tokens: ${model.maxInputTokens ?? 'default'} - - Max Output Tokens: ${model.maxOutputTokens ?? 'default'}`; - }).join('\n\n'); + const modelInfos = await Promise.all(storedModels.map(async model => { + const fields = [ + `- **${model.name}**`, + ` - Provider: ${model.provider}`, + ` - Type: ${model.type}`, + ` - Model ID: ${model.model}`, + ]; + + if (model.toolCalls !== undefined && model.toolCalls !== null) { + fields.push(` - Tool Calls: ${model.toolCalls}`); + } + + if (model.completions !== undefined && model.completions !== null) { + fields.push(` - Completions: ${model.completions}`); + } + + if (model.baseUrl) { + fields.push(` - Base URL: ${model.baseUrl}`); + } + + // Report if an API key is configured + try { + const apiKey = await context.secrets.get(`apiKey-${model.id}`); + if (apiKey) { + fields.push(` - API Key: Yes`); + } + } catch { + // Ignore errors - don't display anything if we can't check + } + + fields.push( + ` - Max Input Tokens: ${model.maxInputTokens ?? `default (${DEFAULT_MAX_TOKEN_INPUT})`}`, + ` - Max Output Tokens: ${model.maxOutputTokens ?? `default (${DEFAULT_MAX_TOKEN_OUTPUT})`}` + ); + + return fields.join('\n'); + })); + + return modelInfos.join('\n\n'); } /** @@ -262,9 +278,9 @@ function getExtensionInfo(): string { /** * Get log output from the Assistant output channel. */ -function getAssistantLogs(includeTraceLevel: boolean = false): string { - const level = includeTraceLevel ? 'trace' : 'debug'; - const logs = log.formatEntriesForDiagnostics(500, level); +function getAssistantLogs(): string { + // Passing trace here will only retrieve trace logs if the user has enabled trace logging + const logs = log.formatEntriesForDiagnostics(500, 'trace'); if (logs === 'No log entries available') { return 'No log entries captured yet. Logs are captured from the moment the extension loads.'; @@ -308,19 +324,6 @@ function getCopilotLogs(): string { } } -/** - * Get Copilot Language Server log reference. - * Note: We can't access Copilot's logs directly as they're in a separate extension. - */ -function getCopilotLogReference(): string { - return `Copilot Language Server logs are managed separately and cannot be captured automatically. - -To include Copilot logs in a bug report: -1. Open the Output panel (View → Output) -2. Select "GitHub Copilot Language Server" from the dropdown -3. Copy relevant log entries manually`; -} - /** * Collect and display comprehensive diagnostics for Positron Assistant. */ @@ -374,7 +377,7 @@ export async function collectDiagnostics(context: vscode.ExtensionContext): Prom // Configured Models await appendText(editor, '## Configured Models\n\n'); - await appendText(editor, getModelInfo(context)); + await appendText(editor, await getModelInfo(context)); await appendText(editor, '\n\n'); // Active Chat Session @@ -384,10 +387,7 @@ export async function collectDiagnostics(context: vscode.ExtensionContext): Prom // Logs await appendText(editor, '## Positron Assistant Logs\n\n'); - await appendText(editor, 'Recent log entries (last 500, debug level and above):\n\n'); - await appendText(editor, '> **Note**: To include trace-level logs, set the log level to "Trace" in:\n'); - await appendText(editor, '> Settings → Extensions → Positron Assistant → Log Level, or\n'); - await appendText(editor, '> Output panel → Assistant → Set Log Level (gear icon)\n\n'); + await appendText(editor, 'Recent log entries (last 500):\n\n'); await appendText(editor, '```\n'); await appendText(editor, getAssistantLogs()); await appendText(editor, '\n```\n\n'); @@ -395,14 +395,9 @@ export async function collectDiagnostics(context: vscode.ExtensionContext): Prom await appendText(editor, '## GitHub Copilot Chat Logs\n\n'); await appendText(editor, 'Recent log entries (last 500, debug level and above):\n\n'); const copilotLogs = getCopilotLogs(); - if (copilotLogs.includes('extension is not active') || copilotLogs.includes('Unable to access')) { - await appendText(editor, `> ${copilotLogs}\n\n`); - await appendText(editor, getCopilotLogReference()); - } else { - await appendText(editor, '```\n'); - await appendText(editor, copilotLogs); - await appendText(editor, '\n```\n'); - } + await appendText(editor, '```\n'); + await appendText(editor, copilotLogs); + await appendText(editor, '\n```\n'); await appendText(editor, '\n\n'); // Footer From 31aaf260a597bcd0604704498b0bc2e7fce8ffac Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Mon, 17 Nov 2025 10:42:29 -0500 Subject: [PATCH 06/13] remove excessive and circular dependencies, consolidate code, save diagnostics to user data dir --- .../positron-assistant/src/diagnostics.ts | 227 +++++++++--------- .../positron-assistant/src/extension.ts | 2 +- extensions/positron-assistant/src/reset.ts | 61 ++--- 3 files changed, 133 insertions(+), 157 deletions(-) diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index 808a8fec999..fd11926afa6 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -6,27 +6,64 @@ import * as vscode from 'vscode'; import * as positron from 'positron'; import { getStoredModels, getEnabledProviders } from './config'; -import { log } from './extension'; import { DEFAULT_MAX_TOKEN_INPUT, DEFAULT_MAX_TOKEN_OUTPUT } from './constants.js'; +import { BufferedLogOutputChannel } from './logBuffer.js'; /** - * Helper function to append text to a document editor. + * Check if a value is an empty array or object that matches the default empty array or object. + * This helps filter out settings that appear different but are functionally the same. + * @param value The current value of the setting + * @param defaultValue The default value of the setting + * @returns True if both are empty arrays or empty objects */ -async function appendText(editor: vscode.TextEditor, text: string): Promise { - await editor.edit(builder => { - const lastLine = editor.document.lineAt(editor.document.lineCount - 1); - builder.insert(lastLine.range.end, text); - }); +function isEmptyArrayOrObjectMatchingDefault(value: unknown, defaultValue: unknown): boolean { + // Check for matching empty arrays + const isBothEmptyArrays = Array.isArray(value) && Array.isArray(defaultValue) && + value.length === 0 && defaultValue.length === 0; + + // Check for matching empty objects (excluding arrays) + const isBothEmptyObjects = typeof value === 'object' && typeof defaultValue === 'object' && + value !== null && defaultValue !== null && + !Array.isArray(value) && !Array.isArray(defaultValue) && + Object.keys(value).length === 0 && Object.keys(defaultValue).length === 0; + + return isBothEmptyArrays || isBothEmptyObjects; } /** - * Get all Positron Assistant configuration settings with non-default values. + * Get configuration settings with non-default values for a given section. + * @param configSection The configuration section to inspect (e.g., 'positron.assistant') + * @param settingKeys Array of setting keys to check + * @returns A formatted string containing settings that differ from their default values */ -function getAssistantSettings(): string { - const config = vscode.workspace.getConfiguration('positron.assistant'); +function getNonDefaultSettings(configSection: string, settingKeys: string[]): string { + const config = vscode.workspace.getConfiguration(configSection); const settings: Record = {}; + for (const key of settingKeys) { + const inspection = config.inspect(key); + const value = config.get(key); + + if (inspection && value !== inspection.defaultValue) { + if (!isEmptyArrayOrObjectMatchingDefault(value, inspection.defaultValue)) { + settings[`${configSection}.${key}`] = value; + } + } + } + + if (Object.keys(settings).length === 0) { + return '\n // No non-default settings configured'; + } + + return '\n' + Object.entries(settings) + .map(([key, value]) => ` "${key}": ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`) + .join(',\n'); +} +/** + * Get all Positron Assistant configuration settings with non-default values. + */ +function getAssistantSettings(): string { // VS Code's API doesn't provide a way to iterate over keys so we maintain a list here const settingKeys = [ 'enable', @@ -53,42 +90,13 @@ function getAssistantSettings(): string { 'enabledProviders', ]; - for (const key of settingKeys) { - const inspection = config.inspect(key); - const value = config.get(key); - - // Only include settings that differ from default - if (inspection && value !== inspection.defaultValue) { - // Check if it's not just an empty array/object matching default empty array/object - const isEmptyArrayOrObject = - (Array.isArray(value) && Array.isArray(inspection.defaultValue) && - value.length === 0 && inspection.defaultValue.length === 0) || - (typeof value === 'object' && typeof inspection.defaultValue === 'object' && - !Array.isArray(value) && !Array.isArray(inspection.defaultValue) && - Object.keys(value).length === 0 && Object.keys(inspection.defaultValue).length === 0); - - if (!isEmptyArrayOrObject) { - settings[`positron.assistant.${key}`] = value; - } - } - } - - if (Object.keys(settings).length === 0) { - return '\n // No non-default settings configured'; - } - - return '\n' + Object.entries(settings) - .map(([key, value]) => ` "${key}": ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`) - .join(',\n'); + return getNonDefaultSettings('positron.assistant', settingKeys); } /** * Get GitHub Copilot Chat configuration settings with non-default values. */ function getCopilotChatSettings(): string { - const config = vscode.workspace.getConfiguration('github.copilot'); - const settings: Record = {}; - // Key Copilot settings that may affect Assistant behavior const settingKeys = [ 'enable', @@ -98,22 +106,7 @@ function getCopilotChatSettings(): string { 'advanced.debug.useNodeFetchFetcher', ]; - for (const key of settingKeys) { - const inspection = config.inspect(key); - const value = config.get(key); - - if (inspection && value !== inspection.defaultValue) { - settings[`github.copilot.${key}`] = value; - } - } - - if (Object.keys(settings).length === 0) { - return '\n // No non-default Copilot settings configured'; - } - - return '\n' + Object.entries(settings) - .map(([key, value]) => ` "${key}": ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`) - .join(',\n'); + return getNonDefaultSettings('github.copilot', settingKeys); } /** @@ -278,7 +271,7 @@ function getExtensionInfo(): string { /** * Get log output from the Assistant output channel. */ -function getAssistantLogs(): string { +function getAssistantLogs(log: BufferedLogOutputChannel): string { // Passing trace here will only retrieve trace logs if the user has enabled trace logging const logs = log.formatEntriesForDiagnostics(500, 'trace'); @@ -311,8 +304,8 @@ function getCopilotLogs(): string { return 'Unable to access Copilot Chat logs (log target not initialized)'; } - // LogLevel.Debug = 2 in Copilot Chat's LogLevel enum (Off=0, Trace=1, Debug=2, Info=3, Warning=4, Error=5) - const logs = logTarget.formatEntriesForDiagnostics(500, 2); + // LogLevel.Trace = 1 in Copilot Chat's LogLevel enum (Off=0, Trace=1, Debug=2, Info=3, Warning=4, Error=5) + const logs = logTarget.formatEntriesForDiagnostics(500, 1); if (logs === 'No log entries available') { return 'No Copilot Chat log entries captured yet'; @@ -325,84 +318,98 @@ function getCopilotLogs(): string { } /** - * Collect and display comprehensive diagnostics for Positron Assistant. + * Generate comprehensive diagnostics content for Positron Assistant. + * @returns The diagnostics content as a markdown string. */ -export async function collectDiagnostics(context: vscode.ExtensionContext): Promise { - // Create a new untitled markdown document - const document = await vscode.workspace.openTextDocument({ - language: 'markdown', - content: '' - }); - const editor = await vscode.window.showTextDocument(document); +export async function generateDiagnosticsContent(context: vscode.ExtensionContext, log: BufferedLogOutputChannel): Promise { + const parts: string[] = []; // Header - await appendText(editor, '# Positron Assistant Diagnostics\n\n'); - await appendText(editor, `Generated: ${new Date().toISOString()}\n\n`); + parts.push('# Positron Assistant Diagnostics\n\n'); + parts.push(`Generated: ${new Date().toISOString()}\n\n`); + parts.push('**Note**: This diagnostic report contains configuration details but does not include secrets or API keys. However, please review it before sharing publicly.\n\n'); // Extension Information - await appendText(editor, '## Extension Information\n\n'); - await appendText(editor, getExtensionInfo()); - await appendText(editor, '\n\n'); + parts.push('## Extension Information\n\n'); + parts.push(getExtensionInfo()); + parts.push('\n\n'); // Configuration Settings - await appendText(editor, '## Configuration Settings\n\n'); - await appendText(editor, '### Positron Assistant Settings (Non-Default)\n\n'); - await appendText(editor, '```json' + getAssistantSettings() + '\n```\n\n'); + parts.push('## Configuration Settings\n\n'); + parts.push('### Positron Assistant Settings (Non-Default)\n\n'); + parts.push('```json' + getAssistantSettings() + '\n```\n\n'); - await appendText(editor, '### GitHub Copilot Settings (Non-Default)\n\n'); - await appendText(editor, '```json' + getCopilotChatSettings() + '\n```\n\n'); + parts.push('### GitHub Copilot Settings (Non-Default)\n\n'); + parts.push('```json' + getCopilotChatSettings() + '\n```\n\n'); // Providers - await appendText(editor, '## Language Model Providers\n\n'); + parts.push('## Language Model Providers\n\n'); - await appendText(editor, '### Enabled Providers (Configuration)\n\n'); + parts.push('### Enabled Providers (Configuration)\n\n'); try { const enabledProviders = await getEnabledProviders(); if (enabledProviders.length === 0) { - await appendText(editor, 'All providers enabled (no filter configured)\n\n'); + parts.push('All providers enabled (no filter configured)\n\n'); } else { - await appendText(editor, enabledProviders.map(p => `- ${p}`).join('\n') + '\n\n'); + parts.push(enabledProviders.map(p => `- ${p}`).join('\n') + '\n\n'); } } catch (error) { - await appendText(editor, `Error: ${error instanceof Error ? error.message : String(error)}\n\n`); + parts.push(`Error: ${error instanceof Error ? error.message : String(error)}\n\n`); } - await appendText(editor, '### Positron Supported Providers\n\n'); - await appendText(editor, await getPositronProviders()); - await appendText(editor, '\n\n'); + parts.push('### Positron Supported Providers\n\n'); + parts.push(await getPositronProviders()); + parts.push('\n\n'); - await appendText(editor, '### Available Models (VS Code Language Model API)\n\n'); - await appendText(editor, await getAvailableProviders()); - await appendText(editor, '\n\n'); + parts.push('### Available Models (VS Code Language Model API)\n\n'); + parts.push(await getAvailableProviders()); + parts.push('\n\n'); // Configured Models - await appendText(editor, '## Configured Models\n\n'); - await appendText(editor, await getModelInfo(context)); - await appendText(editor, '\n\n'); + parts.push('## Configured Models\n\n'); + parts.push(await getModelInfo(context)); + parts.push('\n\n'); // Active Chat Session - await appendText(editor, '## Active Chat Session\n\n'); - await appendText(editor, await getChatExportInfo()); - await appendText(editor, '\n\n'); + parts.push('## Active Chat Session\n\n'); + parts.push(await getChatExportInfo()); + parts.push('\n\n'); // Logs - await appendText(editor, '## Positron Assistant Logs\n\n'); - await appendText(editor, 'Recent log entries (last 500):\n\n'); - await appendText(editor, '```\n'); - await appendText(editor, getAssistantLogs()); - await appendText(editor, '\n```\n\n'); - - await appendText(editor, '## GitHub Copilot Chat Logs\n\n'); - await appendText(editor, 'Recent log entries (last 500, debug level and above):\n\n'); + parts.push('## Positron Assistant Logs\n\n'); + parts.push('Recent log entries (last 500):\n\n'); + parts.push('```\n'); + parts.push(getAssistantLogs(log)); + parts.push('\n```\n\n'); + + parts.push('## GitHub Copilot Chat Logs\n\n'); + parts.push('Recent log entries (last 500):\n\n'); const copilotLogs = getCopilotLogs(); - await appendText(editor, '```\n'); - await appendText(editor, copilotLogs); - await appendText(editor, '\n```\n'); - await appendText(editor, '\n\n'); + parts.push('```\n'); + parts.push(copilotLogs); + parts.push('\n```\n'); + parts.push('\n\n'); // Footer - await appendText(editor, '---\n\n'); - await appendText(editor, '## Documentation\n\n'); - await appendText(editor, '- [Positron Assistant Documentation](https://positron.posit.co/assistant)\n'); - await appendText(editor, '- [Report Issues](https://github.com/posit-dev/positron/issues)\n'); + parts.push('---\n\n'); + parts.push('## Documentation\n\n'); + parts.push('- [Positron Assistant Documentation](https://positron.posit.co/assistant)\n'); + parts.push('- [Report Issues](https://github.com/posit-dev/positron/issues)\n'); + + return parts.join(''); +} + +/** + * Collect and display comprehensive diagnostics for Positron Assistant in a new document. + */ +export async function collectDiagnostics(context: vscode.ExtensionContext, log: BufferedLogOutputChannel): Promise { + const content = await generateDiagnosticsContent(context, log); + + // Create a new untitled markdown document with the content + const document = await vscode.workspace.openTextDocument({ + language: 'markdown', + content: content + }); + + await vscode.window.showTextDocument(document); } diff --git a/extensions/positron-assistant/src/extension.ts b/extensions/positron-assistant/src/extension.ts index 92273dd6560..4ba5773829c 100644 --- a/extensions/positron-assistant/src/extension.ts +++ b/extensions/positron-assistant/src/extension.ts @@ -284,7 +284,7 @@ function registerToggleInlineCompletionsCommand(context: vscode.ExtensionContext function registerCollectDiagnosticsCommand(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('positron-assistant.collectDiagnostics', async () => { - await collectDiagnostics(context); + await collectDiagnostics(context, log); }) ); } diff --git a/extensions/positron-assistant/src/reset.ts b/extensions/positron-assistant/src/reset.ts index 0728d5f6531..b19c398b076 100644 --- a/extensions/positron-assistant/src/reset.ts +++ b/extensions/positron-assistant/src/reset.ts @@ -4,71 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as positron from 'positron'; import * as path from 'path'; -import * as fs from 'fs'; -import { collectDiagnostics } from './diagnostics'; +import { generateDiagnosticsContent } from './diagnostics'; import { CopilotService } from './copilot'; import { PositLanguageModel } from './posit'; import { getStoredModels, GlobalSecretStorage } from './config'; -import { disposeModels } from './extension'; +import { disposeModels, log } from './extension'; /** - * Save diagnostics to a file in a reasonable location. - * Priority: workspace folder > home directory > temp directory + * Save diagnostics to a file in the extension's global storage directory. + * This is located in the user data directory and persists across sessions. */ async function saveDiagnosticsToFile(context: vscode.ExtensionContext): Promise { try { - // First, collect the diagnostics (this opens a new untitled document) - await collectDiagnostics(context); - - // Wait a moment for the diagnostics to finish collecting - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Get the active editor which should be the diagnostics document - const editor = vscode.window.activeTextEditor; - if (!editor) { - throw new Error('No active editor found after collecting diagnostics'); - } - - const content = editor.document.getText(); + // Generate the diagnostics content + const content = await generateDiagnosticsContent(context, log); // Generate timestamp for filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T').join('_').substring(0, 19); const filename = `positron-assistant-diagnostics-${timestamp}.md`; - // Determine save location - let saveDir: string | undefined; + // Use the extension's global storage URI (in user data directory) + const storageUri = context.globalStorageUri; - // Try workspace folder first - const workspaceFolders = vscode.workspace.workspaceFolders; - if (workspaceFolders && workspaceFolders.length > 0) { - saveDir = workspaceFolders[0].uri.fsPath; - } + // Ensure the directory exists + await vscode.workspace.fs.createDirectory(storageUri); - // Fall back to home directory - if (!saveDir) { - const homeDir = process.env.HOME || process.env.USERPROFILE; - if (homeDir) { - saveDir = homeDir; - } - } - - // Last resort: temp directory - if (!saveDir) { - saveDir = process.env.TMPDIR || process.env.TEMP || '/tmp'; - } - - const filePath = path.join(saveDir, filename); + // Create the file URI in the storage directory + const fileUri = vscode.Uri.joinPath(storageUri, filename); // Save to file - fs.writeFileSync(filePath, content, 'utf8'); - - // Close the diagnostics document without prompting to save - // We need to revert the document first to mark it as not dirty - await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); + const fileBuffer = Buffer.from(content, 'utf8'); + await vscode.workspace.fs.writeFile(fileUri, fileBuffer); - return filePath; + return fileUri.fsPath; } catch (error) { vscode.window.showErrorMessage( vscode.l10n.t('Failed to save diagnostics: {0}', error instanceof Error ? error.message : String(error)) From 24752bf2c45811096513add2db50d55756e07fda Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:42:39 -0500 Subject: [PATCH 07/13] add default variables and debug logging --- .../positron-assistant/src/diagnostics.ts | 44 ++++++++++++------- .../positron-assistant/src/logBuffer.ts | 8 ++-- extensions/positron-assistant/src/reset.ts | 10 ++--- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index fd11926afa6..a0fe0c447c3 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -7,7 +7,16 @@ import * as vscode from 'vscode'; import * as positron from 'positron'; import { getStoredModels, getEnabledProviders } from './config'; import { DEFAULT_MAX_TOKEN_INPUT, DEFAULT_MAX_TOKEN_OUTPUT } from './constants.js'; -import { BufferedLogOutputChannel } from './logBuffer.js'; +import { BufferedLogOutputChannel, DIAGNOSTIC_LOG_BUFFER_SIZE } from './logBuffer.js'; + +interface ChatExportData { + requests?: Array<{ + response?: { + agent?: string; + }; + }>; + initialLocation?: string; +} /** * Check if a value is an empty array or object that matches the default empty array or object. @@ -112,7 +121,7 @@ function getCopilotChatSettings(): string { /** * Get information about stored language models and providers. */ -async function getModelInfo(context: vscode.ExtensionContext): Promise { +async function getModelInfo(context: vscode.ExtensionContext, log: BufferedLogOutputChannel): Promise { const storedModels = getStoredModels(context); if (storedModels.length === 0) { @@ -145,8 +154,8 @@ async function getModelInfo(context: vscode.ExtensionContext): Promise { if (apiKey) { fields.push(` - API Key: Yes`); } - } catch { - // Ignore errors - don't display anything if we can't check + } catch (error) { + log.trace(`Failed to check API key for model ${model.id}: ${error instanceof Error ? error.message : String(error)}`); } fields.push( @@ -219,7 +228,7 @@ async function getChatExportInfo(): Promise { } // Cast to access internal structure (API returns object type for stability) - const chatData = chatExport as any; + const chatData = chatExport as ChatExportData; if (!chatData.requests || !Array.isArray(chatData.requests)) { return 'Chat session found but data format is unexpected'; @@ -273,7 +282,7 @@ function getExtensionInfo(): string { */ function getAssistantLogs(log: BufferedLogOutputChannel): string { // Passing trace here will only retrieve trace logs if the user has enabled trace logging - const logs = log.formatEntriesForDiagnostics(500, 'trace'); + const logs = log.formatEntriesForDiagnostics(DIAGNOSTIC_LOG_BUFFER_SIZE); if (logs === 'No log entries available') { return 'No log entries captured yet. Logs are captured from the moment the extension loads.'; @@ -304,8 +313,7 @@ function getCopilotLogs(): string { return 'Unable to access Copilot Chat logs (log target not initialized)'; } - // LogLevel.Trace = 1 in Copilot Chat's LogLevel enum (Off=0, Trace=1, Debug=2, Info=3, Warning=4, Error=5) - const logs = logTarget.formatEntriesForDiagnostics(500, 1); + const logs = logTarget.formatEntriesForDiagnostics(DIAGNOSTIC_LOG_BUFFER_SIZE, COPILOT_LOG_LEVEL_TRACE); if (logs === 'No log entries available') { return 'No Copilot Chat log entries captured yet'; @@ -327,7 +335,13 @@ export async function generateDiagnosticsContent(context: vscode.ExtensionContex // Header parts.push('# Positron Assistant Diagnostics\n\n'); parts.push(`Generated: ${new Date().toISOString()}\n\n`); - parts.push('**Note**: This diagnostic report contains configuration details but does not include secrets or API keys. However, please review it before sharing publicly.\n\n'); + parts.push('**⚠️ Privacy Notice**: This diagnostic report includes:\n'); + parts.push('- Extension versions and configuration settings\n'); + parts.push('- Model configurations (including base URLs and model IDs)\n'); + parts.push('- System information (OS, architecture)\n'); + parts.push('- Recent log entries\n'); + parts.push('- Chat session metadata\n\n'); + parts.push('**The report does NOT include API keys or authentication tokens.** However, base URLs may reveal internal endpoints, and configuration settings might expose security policies. Please review carefully before sharing publicly.\n\n'); // Extension Information parts.push('## Extension Information\n\n'); @@ -345,7 +359,12 @@ export async function generateDiagnosticsContent(context: vscode.ExtensionContex // Providers parts.push('## Language Model Providers\n\n'); - parts.push('### Enabled Providers (Configuration)\n\n'); + // Configured Models + parts.push('## Configured Providers and Models\n\n'); + parts.push(await getModelInfo(context, log)); + parts.push('\n\n'); + + parts.push('### Enabled Providers \n\n'); try { const enabledProviders = await getEnabledProviders(); if (enabledProviders.length === 0) { @@ -365,11 +384,6 @@ export async function generateDiagnosticsContent(context: vscode.ExtensionContex parts.push(await getAvailableProviders()); parts.push('\n\n'); - // Configured Models - parts.push('## Configured Models\n\n'); - parts.push(await getModelInfo(context)); - parts.push('\n\n'); - // Active Chat Session parts.push('## Active Chat Session\n\n'); parts.push(await getChatExportInfo()); diff --git a/extensions/positron-assistant/src/logBuffer.ts b/extensions/positron-assistant/src/logBuffer.ts index f2308ba0403..8981a9cf606 100644 --- a/extensions/positron-assistant/src/logBuffer.ts +++ b/extensions/positron-assistant/src/logBuffer.ts @@ -11,6 +11,8 @@ export interface LogEntry { message: string; } +export const DIAGNOSTIC_LOG_BUFFER_SIZE = 500; + /** * A wrapper around LogOutputChannel that maintains an in-memory circular buffer * of recent log entries for diagnostics collection. @@ -21,7 +23,7 @@ export class BufferedLogOutputChannel implements vscode.LogOutputChannel { constructor( private readonly channel: vscode.LogOutputChannel, - maxEntries: number = 1000 + maxEntries: number = DIAGNOSTIC_LOG_BUFFER_SIZE ) { this.maxEntries = maxEntries; } @@ -144,9 +146,9 @@ export class BufferedLogOutputChannel implements vscode.LogOutputChannel { /** * Format log entries as text for inclusion in diagnostics. * @param count Number of entries to include (default: 500) - * @param level Minimum log level to include (default: 'debug') + * @param level Minimum log level to include (default: 'trace') */ - formatEntriesForDiagnostics(count: number = 500, level: LogEntry['level'] = 'debug'): string { + formatEntriesForDiagnostics(count: number = 500, level: LogEntry['level'] = 'trace'): string { const entries = this.getRecentEntries(count, level); if (entries.length === 0) { diff --git a/extensions/positron-assistant/src/reset.ts b/extensions/positron-assistant/src/reset.ts index b19c398b076..9c65604b9d0 100644 --- a/extensions/positron-assistant/src/reset.ts +++ b/extensions/positron-assistant/src/reset.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as path from 'path'; import { generateDiagnosticsContent } from './diagnostics'; import { CopilotService } from './copilot'; import { PositLanguageModel } from './posit'; @@ -97,8 +96,8 @@ async function clearAssistantState(context: vscode.ExtensionContext): Promise setTimeout(resolve, 500)); // Brief pause vscode.window.showInformationMessage( vscode.l10n.t('Assistant state has been reset. The window will now reload.') From 9ae09456e7a7e4f8a7461513b008e303999db17d Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:43:36 -0500 Subject: [PATCH 08/13] make writing diagnostics option on state reset --- .../positron-assistant/src/diagnostics.ts | 4 +- .../positron-assistant/src/logBuffer.ts | 6 +- extensions/positron-assistant/src/reset.ts | 55 ++++++++++++------- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index a0fe0c447c3..86f230e929a 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -313,7 +313,7 @@ function getCopilotLogs(): string { return 'Unable to access Copilot Chat logs (log target not initialized)'; } - const logs = logTarget.formatEntriesForDiagnostics(DIAGNOSTIC_LOG_BUFFER_SIZE, COPILOT_LOG_LEVEL_TRACE); + const logs = logTarget.formatEntriesForDiagnostics(DIAGNOSTIC_LOG_BUFFER_SIZE); if (logs === 'No log entries available') { return 'No Copilot Chat log entries captured yet'; @@ -335,7 +335,7 @@ export async function generateDiagnosticsContent(context: vscode.ExtensionContex // Header parts.push('# Positron Assistant Diagnostics\n\n'); parts.push(`Generated: ${new Date().toISOString()}\n\n`); - parts.push('**⚠️ Privacy Notice**: This diagnostic report includes:\n'); + parts.push('** Privacy Notice**: This diagnostic report includes:\n'); parts.push('- Extension versions and configuration settings\n'); parts.push('- Model configurations (including base URLs and model IDs)\n'); parts.push('- System information (OS, architecture)\n'); diff --git a/extensions/positron-assistant/src/logBuffer.ts b/extensions/positron-assistant/src/logBuffer.ts index 8981a9cf606..04422ffb1fb 100644 --- a/extensions/positron-assistant/src/logBuffer.ts +++ b/extensions/positron-assistant/src/logBuffer.ts @@ -180,9 +180,9 @@ export class BufferedLogOutputChannel implements vscode.LogOutputChannel { if (args.length === 0) { return message; } - // Simple formatting - VS Code handles the actual formatting for the output channel - return message + (args.length > 0 ? ' ' + args.map(a => + const formattedArgs = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a) - ).join(' ') : ''); + ).join(' '); + return `${message} ${formattedArgs}`; } } diff --git a/extensions/positron-assistant/src/reset.ts b/extensions/positron-assistant/src/reset.ts index 9c65604b9d0..e9a73b98daf 100644 --- a/extensions/positron-assistant/src/reset.ts +++ b/extensions/positron-assistant/src/reset.ts @@ -20,7 +20,8 @@ async function saveDiagnosticsToFile(context: vscode.ExtensionContext): Promise< const content = await generateDiagnosticsContent(context, log); // Generate timestamp for filename - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T').join('_').substring(0, 19); + const timestamp = new Date().toISOString().replace(/T/, '_').replace(/\..+/, '').replace(/:/g, '-'); + const filename = `positron-assistant-diagnostics-${timestamp}.md`; // Use the extension's global storage URI (in user data directory) @@ -77,10 +78,11 @@ async function signOutAllProviders(context: vscode.ExtensionContext): Promise { - // Dispose all registered models + const storedModels = getStoredModels(context); disposeModels(); // Clear global state + // TO DO: REVIEW KEYS TO BE MORE SPECIFIC const globalStateKeys = context.globalState.keys(); for (const key of globalStateKeys) { if (key.startsWith('positron.assistant') || key.includes('assistant')) { @@ -90,7 +92,6 @@ async function clearAssistantState(context: vscode.ExtensionContext): Promise { * Reset all Positron Assistant state. * * This command: - * 1. Generates and saves diagnostic information + * 1. Optionally generates and saves diagnostic information (user choice) * 2. Signs out of all providers (including Copilot) * 3. Clears all Assistant state (global state and secrets) * 4. Deletes all chat history * 5. Reloads the window for a clean start */ export async function resetAssistantState(context: vscode.ExtensionContext): Promise { - // Show confirmation dialog + // Show confirmation dialog with options const result = await vscode.window.showWarningMessage( vscode.l10n.t('Reset Positron Assistant State'), { modal: true, detail: vscode.l10n.t( 'This will:\n' + - '• Generate and save diagnostic information\n' + '• Sign out of all language model providers (including Copilot)\n' + '• Clear all Assistant configuration and state\n' + '• Delete all chat history\n' + @@ -163,14 +163,16 @@ export async function resetAssistantState(context: vscode.ExtensionContext): Pro 'This action cannot be undone.' ) }, - vscode.l10n.t('Reset'), - vscode.l10n.t('Cancel') + vscode.l10n.t('Reset with Diagnostics'), + vscode.l10n.t('Reset without Diagnostics') ); - if (result !== vscode.l10n.t('Reset')) { + if (result === undefined) { return; } + const saveDiagnostics = result === vscode.l10n.t('Reset with Diagnostics'); + // Show progress await vscode.window.withProgress( { @@ -179,29 +181,42 @@ export async function resetAssistantState(context: vscode.ExtensionContext): Pro cancellable: false }, async (progress) => { - // Step 1: Save diagnostics - progress.report({ increment: 0, message: vscode.l10n.t('Saving diagnostics...') }); - const diagnosticsPath = await saveDiagnosticsToFile(context); - if (diagnosticsPath) { - vscode.window.showInformationMessage( - vscode.l10n.t('Diagnostics saved to: {0}', diagnosticsPath) - ); + let currentStep = 0; + const stepIncrement = saveDiagnostics ? 20 : 25; + + // Step 1: Save diagnostics (optional) + if (saveDiagnostics) { + progress.report({ increment: 0, message: vscode.l10n.t('Saving diagnostics...') }); + const diagnosticsPath = await saveDiagnosticsToFile(context); + if (diagnosticsPath) { + vscode.window.showInformationMessage( + vscode.l10n.t('Diagnostics saved to: {0}', diagnosticsPath) + ); + // Open the diagnostics file so it will be restored after reload + const fileUri = vscode.Uri.file(diagnosticsPath); + const document = await vscode.workspace.openTextDocument(fileUri); + await vscode.window.showTextDocument(document, { preview: false }); + } + currentStep += stepIncrement; } // Step 2: Sign out of all providers - progress.report({ increment: 20, message: vscode.l10n.t('Signing out of providers...') }); + progress.report({ increment: currentStep, message: vscode.l10n.t('Signing out of providers...') }); await signOutAllProviders(context); + currentStep += stepIncrement; // Step 3: Clear Assistant state - progress.report({ increment: 40, message: vscode.l10n.t('Clearing Assistant state...') }); + progress.report({ increment: currentStep, message: vscode.l10n.t('Clearing Assistant state...') }); await clearAssistantState(context); + currentStep += stepIncrement; // Step 4: Clear chat history - progress.report({ increment: 60, message: vscode.l10n.t('Clearing chat history...') }); + progress.report({ increment: currentStep, message: vscode.l10n.t('Clearing chat history...') }); await clearChatHistory(); + currentStep += stepIncrement; // Step 5: Reload window - progress.report({ increment: 80, message: vscode.l10n.t('Reloading window...') }); + progress.report({ increment: currentStep, message: vscode.l10n.t('Reloading window...') }); vscode.window.showInformationMessage( vscode.l10n.t('Assistant state has been reset. The window will now reload.') From a15cd8ef84062da2e713c5dd521bfd08e5e4c0b4 Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:42:55 -0500 Subject: [PATCH 09/13] code cleanup --- .../positron-assistant/package.nls.json | 2 +- .../positron-assistant/src/diagnostics.ts | 318 ++++-------------- .../positron-assistant/src/logBuffer.ts | 95 +----- extensions/positron-assistant/src/reset.ts | 127 ++----- 4 files changed, 118 insertions(+), 424 deletions(-) diff --git a/extensions/positron-assistant/package.nls.json b/extensions/positron-assistant/package.nls.json index ff8bc6550b5..f69a0ec6da7 100644 --- a/extensions/positron-assistant/package.nls.json +++ b/extensions/positron-assistant/package.nls.json @@ -11,7 +11,7 @@ "commands.toggleInlineCompletions.title": "Toggle (Enable/Disable) Completions", "commands.managePromptFiles.title": "Manage Prompt Files", "commands.collectDiagnostics.title": "Collect Diagnostics", - "commands.resetState.title": "Reset Assistant State", + "commands.resetState.title": "Reset State", "commands.copilot.signin.title": "Copilot Sign In", "commands.copilot.signout.title": "Copilot Sign Out", "commands.category": "Positron Assistant", diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index 86f230e929a..eae25ec4172 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -5,75 +5,17 @@ import * as vscode from 'vscode'; import * as positron from 'positron'; -import { getStoredModels, getEnabledProviders } from './config'; +import { getStoredModels } from './config'; import { DEFAULT_MAX_TOKEN_INPUT, DEFAULT_MAX_TOKEN_OUTPUT } from './constants.js'; -import { BufferedLogOutputChannel, DIAGNOSTIC_LOG_BUFFER_SIZE } from './logBuffer.js'; - -interface ChatExportData { - requests?: Array<{ - response?: { - agent?: string; - }; - }>; - initialLocation?: string; -} - -/** - * Check if a value is an empty array or object that matches the default empty array or object. - * This helps filter out settings that appear different but are functionally the same. - * @param value The current value of the setting - * @param defaultValue The default value of the setting - * @returns True if both are empty arrays or empty objects - */ -function isEmptyArrayOrObjectMatchingDefault(value: unknown, defaultValue: unknown): boolean { - // Check for matching empty arrays - const isBothEmptyArrays = Array.isArray(value) && Array.isArray(defaultValue) && - value.length === 0 && defaultValue.length === 0; - - // Check for matching empty objects (excluding arrays) - const isBothEmptyObjects = typeof value === 'object' && typeof defaultValue === 'object' && - value !== null && defaultValue !== null && - !Array.isArray(value) && !Array.isArray(defaultValue) && - Object.keys(value).length === 0 && Object.keys(defaultValue).length === 0; - - return isBothEmptyArrays || isBothEmptyObjects; -} - -/** - * Get configuration settings with non-default values for a given section. - * @param configSection The configuration section to inspect (e.g., 'positron.assistant') - * @param settingKeys Array of setting keys to check - * @returns A formatted string containing settings that differ from their default values - */ -function getNonDefaultSettings(configSection: string, settingKeys: string[]): string { - const config = vscode.workspace.getConfiguration(configSection); - const settings: Record = {}; - - for (const key of settingKeys) { - const inspection = config.inspect(key); - const value = config.get(key); - - if (inspection && value !== inspection.defaultValue) { - if (!isEmptyArrayOrObjectMatchingDefault(value, inspection.defaultValue)) { - settings[`${configSection}.${key}`] = value; - } - } - } - - if (Object.keys(settings).length === 0) { - return '\n // No non-default settings configured'; - } +import { BufferedLogOutputChannel } from './logBuffer.js'; - return '\n' + Object.entries(settings) - .map(([key, value]) => ` "${key}": ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`) - .join(',\n'); +function formatError(error: unknown): string { + return error instanceof Error ? error.message : String(error); } -/** - * Get all Positron Assistant configuration settings with non-default values. - */ function getAssistantSettings(): string { // VS Code's API doesn't provide a way to iterate over keys so we maintain a list here + // Alternatively we could iterate on package.json const settingKeys = [ 'enable', 'toolDetails.enable', @@ -99,28 +41,27 @@ function getAssistantSettings(): string { 'enabledProviders', ]; - return getNonDefaultSettings('positron.assistant', settingKeys); -} + const config = vscode.workspace.getConfiguration('positron.assistant'); + const settings: Record = {}; -/** - * Get GitHub Copilot Chat configuration settings with non-default values. - */ -function getCopilotChatSettings(): string { - // Key Copilot settings that may affect Assistant behavior - const settingKeys = [ - 'enable', - 'chat.enableChatCompletion', - 'advanced.debug.useElectronFetcher', - 'advanced.debug.useNodeFetcher', - 'advanced.debug.useNodeFetchFetcher', - ]; + for (const key of settingKeys) { + const inspection = config.inspect(key); + const value = config.get(key); + + if (inspection && value !== inspection.defaultValue) { + settings[`positron.assistant.${key}`] = value; + } + } + + if (Object.keys(settings).length === 0) { + return '\n // No non-default settings configured'; + } - return getNonDefaultSettings('github.copilot', settingKeys); + return '\n' + Object.entries(settings) + .map(([key, value]) => ` "${key}": ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`) + .join(',\n'); } -/** - * Get information about stored language models and providers. - */ async function getModelInfo(context: vscode.ExtensionContext, log: BufferedLogOutputChannel): Promise { const storedModels = getStoredModels(context); @@ -136,11 +77,11 @@ async function getModelInfo(context: vscode.ExtensionContext, log: BufferedLogOu ` - Model ID: ${model.model}`, ]; - if (model.toolCalls !== undefined && model.toolCalls !== null) { + if (model.toolCalls !== undefined) { fields.push(` - Tool Calls: ${model.toolCalls}`); } - if (model.completions !== undefined && model.completions !== null) { + if (model.completions !== undefined) { fields.push(` - Completions: ${model.completions}`); } @@ -155,7 +96,7 @@ async function getModelInfo(context: vscode.ExtensionContext, log: BufferedLogOu fields.push(` - API Key: Yes`); } } catch (error) { - log.trace(`Failed to check API key for model ${model.id}: ${error instanceof Error ? error.message : String(error)}`); + log.trace(`Failed to check API key for model ${model.id}: ${formatError(error)}`); } fields.push( @@ -169,18 +110,14 @@ async function getModelInfo(context: vscode.ExtensionContext, log: BufferedLogOu return modelInfos.join('\n\n'); } -/** - * Get available language model providers from VS Code's Language Model API. - */ -async function getAvailableProviders(): Promise { +async function getAvailableModels(): Promise { try { const models = await vscode.lm.selectChatModels(); if (models.length === 0) { - return 'No language models available through VS Code API'; + return 'No language models available'; } - // Group by vendor const byVendor: Record = {}; for (const model of models) { if (!byVendor[model.vendor]) { @@ -198,28 +135,10 @@ async function getAvailableProviders(): Promise { return sections.join('\n\n'); } catch (error) { - return `Error retrieving models: ${error instanceof Error ? error.message : String(error)}`; - } -} - -/** - * Get enabled providers from Positron AI API. - */ -async function getPositronProviders(): Promise { - try { - const providers = await positron.ai.getSupportedProviders(); - if (providers.length === 0) { - return 'No supported providers from Positron AI'; - } - return providers.map(p => `- ${p}`).join('\n'); - } catch (error) { - return `Error retrieving Positron providers: ${error instanceof Error ? error.message : String(error)}`; + return `Error retrieving models: ${formatError(error)}`; } } -/** - * Get chat export data. - */ async function getChatExportInfo(): Promise { try { const chatExport = await positron.ai.getChatExport(); @@ -228,7 +147,7 @@ async function getChatExportInfo(): Promise { } // Cast to access internal structure (API returns object type for stability) - const chatData = chatExport as ChatExportData; + const chatData = chatExport as any; if (!chatData.requests || !Array.isArray(chatData.requests)) { return 'Chat session found but data format is unexpected'; @@ -240,8 +159,8 @@ async function getChatExportInfo(): Promise { let currentModel = 'Unknown'; if (requestCount > 0) { const lastRequest = chatData.requests[requestCount - 1]; - if (lastRequest.response?.agent) { - currentModel = lastRequest.response.agent; + if (lastRequest.modelId) { + currentModel = lastRequest.modelId; } } @@ -250,14 +169,11 @@ async function getChatExportInfo(): Promise { - Current agent/model: ${currentModel} - Location: ${chatData.initialLocation || 'N/A'}`; } catch (error) { - return `Error retrieving chat export: ${error instanceof Error ? error.message : String(error)}`; + return `Error retrieving chat export: ${formatError(error)}`; } } -/** - * Get extension information. - */ -function getExtensionInfo(): string { +function getVersionInfo(): string { const assistantExt = vscode.extensions.getExtension('positron.positron-assistant'); const copilotExt = vscode.extensions.getExtension('github.copilot-chat'); @@ -277,149 +193,61 @@ function getExtensionInfo(): string { - OS: ${process.platform} ${process.arch}${vscode.env.remoteName ? `\n- Remote: ${vscode.env.remoteName}` : ''}`; } -/** - * Get log output from the Assistant output channel. - */ -function getAssistantLogs(log: BufferedLogOutputChannel): string { - // Passing trace here will only retrieve trace logs if the user has enabled trace logging - const logs = log.formatEntriesForDiagnostics(DIAGNOSTIC_LOG_BUFFER_SIZE); +export async function generateDiagnosticsContent(context: vscode.ExtensionContext, log: BufferedLogOutputChannel): Promise { + return `# Positron Assistant Diagnostics - if (logs === 'No log entries available') { - return 'No log entries captured yet. Logs are captured from the moment the extension loads.'; - } +Generated: ${new Date().toISOString()} - return logs; -} +**Privacy Notice**: This diagnostic report includes: +- Extension versions and configuration settings +- Model configurations (including base URLs and model IDs) +- System information (OS, architecture) +- Recent log entries +- Chat session metadata -/** - * Get Copilot Language Server logs. - * Attempts to access the buffered log target from the Copilot Chat extension. - */ -function getCopilotLogs(): string { - try { - const copilotExt = vscode.extensions.getExtension('github.copilot-chat'); - if (!copilotExt || !copilotExt.isActive) { - return 'GitHub Copilot Chat extension is not active'; - } +**The report does NOT include API keys or authentication tokens.** However, base URLs may reveal internal endpoints, and configuration settings might expose security policies. Please review carefully before sharing. - // Access the log target getter function from the extension exports - const getCopilotLogTarget = (copilotExt.exports as any)?.getCopilotLogTarget; - if (typeof getCopilotLogTarget !== 'function') { - return 'Unable to access Copilot Chat logs (buffered logging may not be enabled)'; - } +## Version Information - const logTarget = getCopilotLogTarget(); - if (!logTarget || typeof logTarget.formatEntriesForDiagnostics !== 'function') { - return 'Unable to access Copilot Chat logs (log target not initialized)'; - } +${getVersionInfo()} - const logs = logTarget.formatEntriesForDiagnostics(DIAGNOSTIC_LOG_BUFFER_SIZE); +## Configuration Settings - if (logs === 'No log entries available') { - return 'No Copilot Chat log entries captured yet'; - } +### Positron Assistant Settings (Non-Default) - return logs; - } catch (error) { - return `Error retrieving Copilot Chat logs: ${error instanceof Error ? error.message : String(error)}`; - } -} +\`\`\`json${getAssistantSettings()} +\`\`\` -/** - * Generate comprehensive diagnostics content for Positron Assistant. - * @returns The diagnostics content as a markdown string. - */ -export async function generateDiagnosticsContent(context: vscode.ExtensionContext, log: BufferedLogOutputChannel): Promise { - const parts: string[] = []; - - // Header - parts.push('# Positron Assistant Diagnostics\n\n'); - parts.push(`Generated: ${new Date().toISOString()}\n\n`); - parts.push('** Privacy Notice**: This diagnostic report includes:\n'); - parts.push('- Extension versions and configuration settings\n'); - parts.push('- Model configurations (including base URLs and model IDs)\n'); - parts.push('- System information (OS, architecture)\n'); - parts.push('- Recent log entries\n'); - parts.push('- Chat session metadata\n\n'); - parts.push('**The report does NOT include API keys or authentication tokens.** However, base URLs may reveal internal endpoints, and configuration settings might expose security policies. Please review carefully before sharing publicly.\n\n'); - - // Extension Information - parts.push('## Extension Information\n\n'); - parts.push(getExtensionInfo()); - parts.push('\n\n'); - - // Configuration Settings - parts.push('## Configuration Settings\n\n'); - parts.push('### Positron Assistant Settings (Non-Default)\n\n'); - parts.push('```json' + getAssistantSettings() + '\n```\n\n'); - - parts.push('### GitHub Copilot Settings (Non-Default)\n\n'); - parts.push('```json' + getCopilotChatSettings() + '\n```\n\n'); - - // Providers - parts.push('## Language Model Providers\n\n'); - - // Configured Models - parts.push('## Configured Providers and Models\n\n'); - parts.push(await getModelInfo(context, log)); - parts.push('\n\n'); - - parts.push('### Enabled Providers \n\n'); - try { - const enabledProviders = await getEnabledProviders(); - if (enabledProviders.length === 0) { - parts.push('All providers enabled (no filter configured)\n\n'); - } else { - parts.push(enabledProviders.map(p => `- ${p}`).join('\n') + '\n\n'); - } - } catch (error) { - parts.push(`Error: ${error instanceof Error ? error.message : String(error)}\n\n`); - } +## Language Model Providers + +## Configured Providers + +${await getModelInfo(context, log)} + +### Available Models + +${await getAvailableModels()} + +## Active Chat Session + +${await getChatExportInfo()} + +## Positron Assistant Logs + +Recent log entries (last 500): + +\`\`\` +${log.formatEntriesForDiagnostics()} +\`\`\` + +--- - parts.push('### Positron Supported Providers\n\n'); - parts.push(await getPositronProviders()); - parts.push('\n\n'); - - parts.push('### Available Models (VS Code Language Model API)\n\n'); - parts.push(await getAvailableProviders()); - parts.push('\n\n'); - - // Active Chat Session - parts.push('## Active Chat Session\n\n'); - parts.push(await getChatExportInfo()); - parts.push('\n\n'); - - // Logs - parts.push('## Positron Assistant Logs\n\n'); - parts.push('Recent log entries (last 500):\n\n'); - parts.push('```\n'); - parts.push(getAssistantLogs(log)); - parts.push('\n```\n\n'); - - parts.push('## GitHub Copilot Chat Logs\n\n'); - parts.push('Recent log entries (last 500):\n\n'); - const copilotLogs = getCopilotLogs(); - parts.push('```\n'); - parts.push(copilotLogs); - parts.push('\n```\n'); - parts.push('\n\n'); - - // Footer - parts.push('---\n\n'); - parts.push('## Documentation\n\n'); - parts.push('- [Positron Assistant Documentation](https://positron.posit.co/assistant)\n'); - parts.push('- [Report Issues](https://github.com/posit-dev/positron/issues)\n'); - - return parts.join(''); +`; } -/** - * Collect and display comprehensive diagnostics for Positron Assistant in a new document. - */ export async function collectDiagnostics(context: vscode.ExtensionContext, log: BufferedLogOutputChannel): Promise { const content = await generateDiagnosticsContent(context, log); - // Create a new untitled markdown document with the content const document = await vscode.workspace.openTextDocument({ language: 'markdown', content: content diff --git a/extensions/positron-assistant/src/logBuffer.ts b/extensions/positron-assistant/src/logBuffer.ts index 04422ffb1fb..9b731e668fd 100644 --- a/extensions/positron-assistant/src/logBuffer.ts +++ b/extensions/positron-assistant/src/logBuffer.ts @@ -11,24 +11,18 @@ export interface LogEntry { message: string; } -export const DIAGNOSTIC_LOG_BUFFER_SIZE = 500; - /** * A wrapper around LogOutputChannel that maintains an in-memory circular buffer * of recent log entries for diagnostics collection. */ export class BufferedLogOutputChannel implements vscode.LogOutputChannel { private readonly buffer: LogEntry[] = []; - private readonly maxEntries: number; constructor( private readonly channel: vscode.LogOutputChannel, - maxEntries: number = DIAGNOSTIC_LOG_BUFFER_SIZE - ) { - this.maxEntries = maxEntries; - } + private readonly maxEntries: number = 500 + ) { } - // Implement LogOutputChannel interface by delegating to the wrapped channel get logLevel(): vscode.LogLevel { return this.channel.logLevel; } @@ -60,11 +54,7 @@ export class BufferedLogOutputChannel implements vscode.LogOutputChannel { show(preserveFocus?: boolean): void; show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { - if (typeof columnOrPreserveFocus === 'boolean') { - this.channel.show(columnOrPreserveFocus); - } else { - this.channel.show(columnOrPreserveFocus, preserveFocus); - } + this.channel.show(columnOrPreserveFocus as any, preserveFocus); } hide(): void { @@ -82,107 +72,56 @@ export class BufferedLogOutputChannel implements vscode.LogOutputChannel { message }); - // Keep buffer size under limit (circular buffer behavior) if (this.buffer.length > this.maxEntries) { this.buffer.shift(); } } + private formatMessageWithArgs(message: string, args: any[]): string { + return args.length > 0 ? `${message} ${args.join(' ')}` : message; + } + trace(message: string, ...args: any[]): void { - const formattedMessage = this.formatMessage(message, args); - this.addToBuffer('trace', formattedMessage); + this.addToBuffer('trace', this.formatMessageWithArgs(message, args)); this.channel.trace(message, ...args); } debug(message: string, ...args: any[]): void { - const formattedMessage = this.formatMessage(message, args); - this.addToBuffer('debug', formattedMessage); + this.addToBuffer('debug', this.formatMessageWithArgs(message, args)); this.channel.debug(message, ...args); } info(message: string, ...args: any[]): void { - const formattedMessage = this.formatMessage(message, args); - this.addToBuffer('info', formattedMessage); + this.addToBuffer('info', this.formatMessageWithArgs(message, args)); this.channel.info(message, ...args); } warn(message: string, ...args: any[]): void { - const formattedMessage = this.formatMessage(message, args); - this.addToBuffer('warn', formattedMessage); + this.addToBuffer('warn', this.formatMessageWithArgs(message, args)); this.channel.warn(message, ...args); } error(message: string | Error, ...args: any[]): void { const formattedMessage = message instanceof Error ? `${message.message}\n${message.stack}` - : this.formatMessage(message, args); + : this.formatMessageWithArgs(message, args); this.addToBuffer('error', formattedMessage); this.channel.error(message, ...args); } - /** - * Get recent log entries from the buffer. - * @param count Number of entries to retrieve (default: all) - * @param level Minimum log level to include (default: all) - */ - getRecentEntries(count?: number, level?: LogEntry['level']): LogEntry[] { - let entries = [...this.buffer]; - - // Filter by level if specified - if (level) { - const levels: LogEntry['level'][] = ['trace', 'debug', 'info', 'warn', 'error']; - const minLevelIndex = levels.indexOf(level); - entries = entries.filter(entry => levels.indexOf(entry.level) >= minLevelIndex); - } - - // Limit count if specified - if (count !== undefined && count < entries.length) { - entries = entries.slice(-count); - } - - return entries; - } - - /** - * Format log entries as text for inclusion in diagnostics. - * @param count Number of entries to include (default: 500) - * @param level Minimum log level to include (default: 'trace') - */ - formatEntriesForDiagnostics(count: number = 500, level: LogEntry['level'] = 'trace'): string { - const entries = this.getRecentEntries(count, level); + formatEntriesForDiagnostics(count: number = 500): string { + const entries = count < this.buffer.length + ? this.buffer.slice(-count) + : this.buffer; if (entries.length === 0) { return 'No log entries available'; } - const formatted = entries.map(entry => { + return entries.map(entry => { const timestamp = entry.timestamp.toISOString(); const levelStr = entry.level.toUpperCase().padEnd(5); return `[${timestamp}] ${levelStr} ${entry.message}`; }).join('\n'); - - const totalInBuffer = this.buffer.length; - const note = entries.length < totalInBuffer - ? `\n\n(Showing ${entries.length} of ${totalInBuffer} total entries in buffer)` - : ''; - - return formatted + note; - } - - /** - * Clear the log buffer. - */ - clearBuffer(): void { - this.buffer.length = 0; - } - - private formatMessage(message: string, args: any[]): string { - if (args.length === 0) { - return message; - } - const formattedArgs = args.map(a => - typeof a === 'object' ? JSON.stringify(a) : String(a) - ).join(' '); - return `${message} ${formattedArgs}`; } } diff --git a/extensions/positron-assistant/src/reset.ts b/extensions/positron-assistant/src/reset.ts index e9a73b98daf..e36d2d3c5ff 100644 --- a/extensions/positron-assistant/src/reset.ts +++ b/extensions/positron-assistant/src/reset.ts @@ -6,134 +6,66 @@ import * as vscode from 'vscode'; import { generateDiagnosticsContent } from './diagnostics'; import { CopilotService } from './copilot'; -import { PositLanguageModel } from './posit'; import { getStoredModels, GlobalSecretStorage } from './config'; import { disposeModels, log } from './extension'; -/** - * Save diagnostics to a file in the extension's global storage directory. - * This is located in the user data directory and persists across sessions. - */ +function formatError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + async function saveDiagnosticsToFile(context: vscode.ExtensionContext): Promise { try { - // Generate the diagnostics content const content = await generateDiagnosticsContent(context, log); - // Generate timestamp for filename - const timestamp = new Date().toISOString().replace(/T/, '_').replace(/\..+/, '').replace(/:/g, '-'); - + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `positron-assistant-diagnostics-${timestamp}.md`; - - // Use the extension's global storage URI (in user data directory) const storageUri = context.globalStorageUri; // Ensure the directory exists await vscode.workspace.fs.createDirectory(storageUri); - // Create the file URI in the storage directory const fileUri = vscode.Uri.joinPath(storageUri, filename); - - // Save to file const fileBuffer = Buffer.from(content, 'utf8'); + await vscode.workspace.fs.writeFile(fileUri, fileBuffer); return fileUri.fsPath; } catch (error) { vscode.window.showErrorMessage( - vscode.l10n.t('Failed to save diagnostics: {0}', error instanceof Error ? error.message : String(error)) + vscode.l10n.t('Failed to save diagnostics: {0}', formatError(error)) ); return undefined; } } -/** - * Sign out of all providers and clear their authentication state. - */ -async function signOutAllProviders(context: vscode.ExtensionContext): Promise { - const storage = new GlobalSecretStorage(context); - const storedModels = getStoredModels(context); - - // Track unique providers to sign out - const providers = new Set(); - storedModels.forEach(model => providers.add(model.provider)); - - // Sign out of each provider - for (const provider of providers) { - try { - if (provider === 'copilot') { - await CopilotService.instance().signOut(); - } else if (provider === 'posit') { - await PositLanguageModel.signOut(storage); - } - // Other providers that use OAuth or API keys will be cleared via secret storage - } catch (error) { - vscode.window.showWarningMessage( - vscode.l10n.t('Failed to sign out of {0}: {1}', provider, error instanceof Error ? error.message : String(error)) - ); - } - } -} - -/** - * Clear all Assistant state from global state and secret storage. - */ async function clearAssistantState(context: vscode.ExtensionContext): Promise { const storedModels = getStoredModels(context); disposeModels(); - // Clear global state - // TO DO: REVIEW KEYS TO BE MORE SPECIFIC const globalStateKeys = context.globalState.keys(); for (const key of globalStateKeys) { - if (key.startsWith('positron.assistant') || key.includes('assistant')) { + if (key.startsWith('positron.assistant')) { await context.globalState.update(key, undefined); } } - // Clear secret storage - we need to clear known secret keys const storage = new GlobalSecretStorage(context); - - // Clear API keys for all stored models for (const model of storedModels) { try { await storage.delete(`apiKey-${model.id}`); } catch (error) { - log.trace(`Failed to delete API key for model ${model.id}: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // Clear the models list itself - await context.globalState.update('positron.assistant.models', undefined); - - // Clear other known secret keys - const knownSecrets = [ - 'posit.token', - 'posit.refreshToken', - 'copilot.token' - ]; - - for (const secret of knownSecrets) { - try { - await storage.delete(secret); - } catch (error) { - log.trace(`Failed to delete secret ${secret}: ${error instanceof Error ? error.message : String(error)}`); + log.trace(`Failed to delete API key for model ${model.id}: ${formatError(error)}`); } } } -/** - * Clear all chat history and sessions. - */ async function clearChatHistory(): Promise { try { - // Use VS Code's built-in command to clear all chat history await vscode.commands.executeCommand('workbench.action.chat.clearHistory'); - - // Also clear the chat widget input history await vscode.commands.executeCommand('workbench.action.chat.clearInputHistory'); } catch (error) { vscode.window.showWarningMessage( - vscode.l10n.t('Failed to clear chat history: {0}', error instanceof Error ? error.message : String(error)) + vscode.l10n.t('Failed to clear chat history: {0}', formatError(error)) ); } } @@ -143,37 +75,40 @@ async function clearChatHistory(): Promise { * * This command: * 1. Optionally generates and saves diagnostic information (user choice) - * 2. Signs out of all providers (including Copilot) + * 2. Signs out of all providers * 3. Clears all Assistant state (global state and secrets) * 4. Deletes all chat history * 5. Reloads the window for a clean start */ export async function resetAssistantState(context: vscode.ExtensionContext): Promise { // Show confirmation dialog with options + const resetWithDiagnostics = vscode.l10n.t('Reset with Diagnostics'); + const resetWithoutDiagnostics = vscode.l10n.t('Reset without Diagnostics'); + const result = await vscode.window.showWarningMessage( vscode.l10n.t('Reset Positron Assistant State'), { modal: true, detail: vscode.l10n.t( 'This will:\n' + - '• Sign out of all language model providers (including Copilot)\n' + + '• Sign out of all language model providers\n' + '• Clear all Assistant configuration and state\n' + '• Delete all chat history\n' + '• Reload the window\n\n' + - 'This action cannot be undone.' + 'This action cannot be undone.\n\n' + + 'You can optionally save diagnostic information before resetting. This may be helpful for troubleshooting issues. The diagnostics file will be opened after the window reloads.' ) }, - vscode.l10n.t('Reset with Diagnostics'), - vscode.l10n.t('Reset without Diagnostics') + resetWithDiagnostics, + resetWithoutDiagnostics ); if (result === undefined) { return; } - const saveDiagnostics = result === vscode.l10n.t('Reset with Diagnostics'); + const saveDiagnostics = result === resetWithDiagnostics; - // Show progress await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, @@ -181,12 +116,9 @@ export async function resetAssistantState(context: vscode.ExtensionContext): Pro cancellable: false }, async (progress) => { - let currentStep = 0; - const stepIncrement = saveDiagnostics ? 20 : 25; - // Step 1: Save diagnostics (optional) if (saveDiagnostics) { - progress.report({ increment: 0, message: vscode.l10n.t('Saving diagnostics...') }); + progress.report({ message: vscode.l10n.t('Saving diagnostics...') }); const diagnosticsPath = await saveDiagnosticsToFile(context); if (diagnosticsPath) { vscode.window.showInformationMessage( @@ -197,32 +129,27 @@ export async function resetAssistantState(context: vscode.ExtensionContext): Pro const document = await vscode.workspace.openTextDocument(fileUri); await vscode.window.showTextDocument(document, { preview: false }); } - currentStep += stepIncrement; } - // Step 2: Sign out of all providers - progress.report({ increment: currentStep, message: vscode.l10n.t('Signing out of providers...') }); - await signOutAllProviders(context); - currentStep += stepIncrement; + // Step 2: Sign out of providers + progress.report({ message: vscode.l10n.t('Signing out of providers...') }); + await CopilotService.instance().signOut(); // Step 3: Clear Assistant state - progress.report({ increment: currentStep, message: vscode.l10n.t('Clearing Assistant state...') }); + progress.report({ message: vscode.l10n.t('Clearing Assistant state...') }); await clearAssistantState(context); - currentStep += stepIncrement; // Step 4: Clear chat history - progress.report({ increment: currentStep, message: vscode.l10n.t('Clearing chat history...') }); + progress.report({ message: vscode.l10n.t('Clearing chat history...') }); await clearChatHistory(); - currentStep += stepIncrement; // Step 5: Reload window - progress.report({ increment: currentStep, message: vscode.l10n.t('Reloading window...') }); + progress.report({ message: vscode.l10n.t('Reloading window...') }); vscode.window.showInformationMessage( vscode.l10n.t('Assistant state has been reset. The window will now reload.') ); - // Reload the window await vscode.commands.executeCommand('workbench.action.reloadWindow'); } ); From f7cc9c1dcba82f1fddd66f950b0f7cf73bb1987d Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:50:46 -0500 Subject: [PATCH 10/13] include copilot settings in assistant diagnostics and notify user via modal after clearing state before reload --- .../positron-assistant/src/diagnostics.ts | 93 ++++++++++++------- extensions/positron-assistant/src/reset.ts | 3 +- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index eae25ec4172..b431d5fed75 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -13,51 +13,72 @@ function formatError(error: unknown): string { return error instanceof Error ? error.message : String(error); } -function getAssistantSettings(): string { - // VS Code's API doesn't provide a way to iterate over keys so we maintain a list here - // Alternatively we could iterate on package.json - const settingKeys = [ - 'enable', - 'toolDetails.enable', - 'useAnthropicSdk', - 'streamingEdits.enable', - 'inlineCompletions.enable', - 'inlineCompletionExcludes', - 'gitIntegration.enable', - 'showTokenUsage.enable', - 'maxInputTokens', - 'maxOutputTokens', - 'followups.enable', - 'consoleActions.enable', - 'notebookMode.enable', - 'toolErrors.propagate', - 'alwaysIncludeCopilotTools', - 'providerTimeout', - 'maxConnectionAttempts', - 'filterModels', - 'preferredModel', - 'defaultModels', - 'providerVariables.bedrock', - 'enabledProviders', - ]; - - const config = vscode.workspace.getConfiguration('positron.assistant'); +/** + * Retrieves non-default settings for an extension. + * @param extensionId The full extension identifier (e.g., 'positron.positron-assistant') + * @param configPrefix The configuration prefix to filter by (e.g., 'positron.assistant') + * @param hiddenSettingKeys Optional array of setting keys not declared in package.json + * @returns Record of non-default settings with their values + */ +function getExtensionSettings( + extensionId: string, + configPrefix: string, + hiddenSettingKeys: string[] = [] +): Record { + const extension = vscode.extensions.getExtension(extensionId); + const settingKeys: string[] = []; + + if (extension?.packageJSON?.contributes?.configuration) { + const configurations = Array.isArray(extension.packageJSON.contributes.configuration) + ? extension.packageJSON.contributes.configuration + : [extension.packageJSON.contributes.configuration]; + + for (const config of configurations) { + if (config.properties) { + for (const key of Object.keys(config.properties)) { + if (key.startsWith(configPrefix + '.')) { + settingKeys.push(key.substring(configPrefix.length + 1)); + } + } + } + } + } + + const allSettingKeys = [...settingKeys, ...hiddenSettingKeys]; + const config = vscode.workspace.getConfiguration(configPrefix); const settings: Record = {}; - for (const key of settingKeys) { + for (const key of allSettingKeys) { const inspection = config.inspect(key); const value = config.get(key); if (inspection && value !== inspection.defaultValue) { - settings[`positron.assistant.${key}`] = value; + settings[`${configPrefix}.${key}`] = value; } } - if (Object.keys(settings).length === 0) { + return settings; +} + +function getRelatedSettings(): string { + const assistantSettings = getExtensionSettings( + 'positron.positron-assistant', + 'positron.assistant', + ['enabledProviders'] + ); + + const copilotSettings = getExtensionSettings( + 'github.copilot-chat', + 'github.copilot' + ); + + const allSettings = { ...assistantSettings, ...copilotSettings }; + + if (Object.keys(allSettings).length === 0) { return '\n // No non-default settings configured'; } - return '\n' + Object.entries(settings) + return '\n' + Object.entries(allSettings) .map(([key, value]) => ` "${key}": ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`) .join(',\n'); } @@ -213,9 +234,11 @@ ${getVersionInfo()} ## Configuration Settings -### Positron Assistant Settings (Non-Default) +### Extension Settings + +Positron Assistant and GitHub Copilot settings: -\`\`\`json${getAssistantSettings()} +\`\`\`json${getRelatedSettings()} \`\`\` ## Language Model Providers diff --git a/extensions/positron-assistant/src/reset.ts b/extensions/positron-assistant/src/reset.ts index e36d2d3c5ff..66afb171914 100644 --- a/extensions/positron-assistant/src/reset.ts +++ b/extensions/positron-assistant/src/reset.ts @@ -147,7 +147,8 @@ export async function resetAssistantState(context: vscode.ExtensionContext): Pro progress.report({ message: vscode.l10n.t('Reloading window...') }); vscode.window.showInformationMessage( - vscode.l10n.t('Assistant state has been reset. The window will now reload.') + vscode.l10n.t('Assistant state has been reset. The window will now reload.'), + { modal: true } ); await vscode.commands.executeCommand('workbench.action.reloadWindow'); From ed164609f41fd2a2dfcff8ec8fd3530fc5544de4 Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:54:33 -0500 Subject: [PATCH 11/13] remove default model from configured providers list as it introduces more confusion than help --- extensions/positron-assistant/src/diagnostics.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index b431d5fed75..d1ecc456e74 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -95,7 +95,6 @@ async function getModelInfo(context: vscode.ExtensionContext, log: BufferedLogOu `- **${model.name}**`, ` - Provider: ${model.provider}`, ` - Type: ${model.type}`, - ` - Model ID: ${model.model}`, ]; if (model.toolCalls !== undefined) { From 36e55100065af68dc601f9cdb9542bc47f07015f Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:37:16 -0500 Subject: [PATCH 12/13] add providers that were configured by environment variable and consolidate types --- .../positron-assistant/src/diagnostics.ts | 113 ++++++++++++------ 1 file changed, 77 insertions(+), 36 deletions(-) diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index d1ecc456e74..36fb55b698b 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -8,6 +8,7 @@ import * as positron from 'positron'; import { getStoredModels } from './config'; import { DEFAULT_MAX_TOKEN_INPUT, DEFAULT_MAX_TOKEN_OUTPUT } from './constants.js'; import { BufferedLogOutputChannel } from './logBuffer.js'; +import { getLanguageModels } from './models.js'; function formatError(error: unknown): string { return error instanceof Error ? error.message : String(error); @@ -83,51 +84,60 @@ function getRelatedSettings(): string { .join(',\n'); } -async function getModelInfo(context: vscode.ExtensionContext, log: BufferedLogOutputChannel): Promise { +async function getConfiguredProviders(context: vscode.ExtensionContext, log: BufferedLogOutputChannel): Promise { const storedModels = getStoredModels(context); - - if (storedModels.length === 0) { - return 'No models configured'; + const envModels = getEnvironmentConfiguredModels(); + + const modelsByProvider = new Map(); + for (const model of storedModels) { + const existing = modelsByProvider.get(model.provider); + if (existing) { + existing.push(model); + } else { + modelsByProvider.set(model.provider, [model]); + } } - const modelInfos = await Promise.all(storedModels.map(async model => { - const fields = [ - `- **${model.name}**`, - ` - Provider: ${model.provider}`, - ` - Type: ${model.type}`, - ]; - - if (model.toolCalls !== undefined) { - fields.push(` - Tool Calls: ${model.toolCalls}`); - } + const modelInfos = await Promise.all( + Array.from(modelsByProvider.entries()).map(async ([provider, models]) => { + // Use the first model for shared properties + const firstModel = models[0]; + const types = [...new Set(models.map(m => m.type))]; - if (model.completions !== undefined) { - fields.push(` - Completions: ${model.completions}`); - } + const fields = [ + `- **${firstModel.name}**`, + ` - Provider: ${provider}`, + ` - Types: ${types.join(', ')}`, + ]; - if (model.baseUrl) { - fields.push(` - Base URL: ${model.baseUrl}`); - } + if (firstModel.baseUrl) { + fields.push(` - Base URL: ${firstModel.baseUrl}`); + } - // Report if an API key is configured - try { - const apiKey = await context.secrets.get(`apiKey-${model.id}`); - if (apiKey) { - fields.push(` - API Key: Yes`); + // Report if an API key is configured (check first model's ID) + try { + const apiKey = await context.secrets.get(`apiKey-${firstModel.id}`); + if (apiKey) { + fields.push(` - API Key: Yes`); + } + } catch (error) { + log.trace(`Failed to check API key for model ${firstModel.id}: ${formatError(error)}`); } - } catch (error) { - log.trace(`Failed to check API key for model ${model.id}: ${formatError(error)}`); - } - fields.push( - ` - Max Input Tokens: ${model.maxInputTokens ?? `default (${DEFAULT_MAX_TOKEN_INPUT})`}`, - ` - Max Output Tokens: ${model.maxOutputTokens ?? `default (${DEFAULT_MAX_TOKEN_OUTPUT})`}` - ); + return fields.join('\n'); + }) + ); + + const allModels = [...modelInfos]; + if (envModels) { + allModels.push(envModels); + } - return fields.join('\n'); - })); + if (allModels.length === 0) { + return 'No models configured'; + } - return modelInfos.join('\n\n'); + return allModels.join('\n\n'); } async function getAvailableModels(): Promise { @@ -193,6 +203,37 @@ async function getChatExportInfo(): Promise { } } +function getEnvironmentConfiguredModels(): string { + const models = getLanguageModels(); + const envModels = models + .filter(model => { + const defaults = model.source.defaults as any; + if (!('apiKeyEnvVar' in defaults && defaults.apiKeyEnvVar)) { + return false; + } + const envVarConfig = defaults.apiKeyEnvVar as { key: string; signedIn: boolean }; + const key = envVarConfig.key; + // Only include if the environment variable is actually set + return !!process.env[key]; + }) + .map(model => { + const defaults = model.source.defaults as any; + const envVarConfig = defaults.apiKeyEnvVar as { key: string; signedIn: boolean }; + const key = envVarConfig.key; + + const fields = [ + `- **${model.source.provider.displayName}**`, + `\t- Provider: ${model.source.provider.id}`, + `\t- Type: ${model.source.type}`, + `\t- API Key: Yes (\`${key}\`)`, + ]; + + return fields.join('\n'); + }); + + return envModels.length > 0 ? envModels.join('\n\n') : ''; +} + function getVersionInfo(): string { const assistantExt = vscode.extensions.getExtension('positron.positron-assistant'); const copilotExt = vscode.extensions.getExtension('github.copilot-chat'); @@ -244,7 +285,7 @@ Positron Assistant and GitHub Copilot settings: ## Configured Providers -${await getModelInfo(context, log)} +${await getConfiguredProviders(context, log)} ### Available Models From da4f32d0e3bea7e548ed908add2d828032a5e19d Mon Sep 17 00:00:00 2001 From: Melissa Barca <5323711+melissa-barca@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:42:59 -0500 Subject: [PATCH 13/13] add TODO about chat info --- extensions/positron-assistant/src/diagnostics.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/positron-assistant/src/diagnostics.ts b/extensions/positron-assistant/src/diagnostics.ts index 36fb55b698b..0958ba66c4e 100644 --- a/extensions/positron-assistant/src/diagnostics.ts +++ b/extensions/positron-assistant/src/diagnostics.ts @@ -171,6 +171,7 @@ async function getAvailableModels(): Promise { async function getChatExportInfo(): Promise { try { + // TODO: This returns the last focused chat, we may want to update to include all chats const chatExport = await positron.ai.getChatExport(); if (!chatExport) { return 'No active chat session';